diff --git a/docs/components/AWS.mdx b/docs/components/AWS.mdx
index d79672bd00..c6d6885e43 100644
--- a/docs/components/AWS.mdx
+++ b/docs/components/AWS.mdx
@@ -73,6 +73,9 @@ import { CardGrid, LinkCard } from "@astrojs/starlight/components";
+
+
+
@@ -3021,6 +3024,98 @@ The Upsert DNS Record component creates or updates a DNS record in an AWS Route
}
```
+
+
+## S3 • Create Bucket
+
+**Component key:** `aws.s3.createBucket`
+
+The Create Bucket component creates a new AWS S3 bucket.
+
+### Configuration
+
+- **Region**: AWS region where the bucket will be created
+- **Bucket Name**: Globally unique name for the new bucket (3-63 characters, lowercase)
+
+### Output
+
+Emits the created bucket's name, region, and ARN on the default channel.
+
+### Example Output
+
+```json
+{
+ "data": {
+ "arn": "arn:aws:s3:::my-example-bucket",
+ "bucketName": "my-example-bucket",
+ "region": "us-east-1"
+ },
+ "timestamp": "2026-02-11T12:00:00Z",
+ "type": "aws.s3.bucket"
+}
+```
+
+
+
+## S3 • Delete Bucket
+
+**Component key:** `aws.s3.deleteBucket`
+
+The Delete Bucket component deletes an AWS S3 bucket. The bucket must be empty.
+
+### Configuration
+
+- **Region**: AWS region of the S3 bucket
+- **Bucket**: Target S3 bucket to delete
+
+### Output
+
+Emits the deleted bucket's name and a deletion confirmation on the default channel.
+
+### Example Output
+
+```json
+{
+ "data": {
+ "bucketName": "my-example-bucket",
+ "deleted": true
+ },
+ "timestamp": "2026-02-11T12:00:00Z",
+ "type": "aws.s3.bucket.deleted"
+}
+```
+
+
+
+## S3 • Get Bucket
+
+**Component key:** `aws.s3.getBucket`
+
+The Get Bucket component retrieves metadata for an AWS S3 bucket.
+
+### Configuration
+
+- **Region**: AWS region of the S3 bucket
+- **Bucket**: Target S3 bucket
+
+### Output
+
+Emits the bucket's name, resolved region, and ARN on the default channel.
+
+### Example Output
+
+```json
+{
+ "data": {
+ "arn": "arn:aws:s3:::my-example-bucket",
+ "bucketName": "my-example-bucket",
+ "region": "eu-west-1"
+ },
+ "timestamp": "2026-02-11T12:00:00Z",
+ "type": "aws.s3.bucket"
+}
+```
+
## SNS • Create Topic
diff --git a/pkg/integrations/aws/aws.go b/pkg/integrations/aws/aws.go
index a5d563743a..b399be7c35 100644
--- a/pkg/integrations/aws/aws.go
+++ b/pkg/integrations/aws/aws.go
@@ -28,6 +28,7 @@ import (
"github.com/superplanehq/superplane/pkg/integrations/aws/lambda"
"github.com/superplanehq/superplane/pkg/integrations/aws/prometheus"
"github.com/superplanehq/superplane/pkg/integrations/aws/route53"
+ "github.com/superplanehq/superplane/pkg/integrations/aws/s3"
"github.com/superplanehq/superplane/pkg/integrations/aws/sns"
"github.com/superplanehq/superplane/pkg/integrations/aws/sqs"
"github.com/superplanehq/superplane/pkg/registry"
@@ -198,6 +199,9 @@ func (a *AWS) Actions() []core.Action {
&route53.CreateRecord{},
&route53.UpsertRecord{},
&route53.DeleteRecord{},
+ &s3.CreateBucket{},
+ &s3.GetBucket{},
+ &s3.DeleteBucket{},
}
}
diff --git a/pkg/integrations/aws/resources.go b/pkg/integrations/aws/resources.go
index 572c9a1037..e08d21f23c 100644
--- a/pkg/integrations/aws/resources.go
+++ b/pkg/integrations/aws/resources.go
@@ -10,6 +10,7 @@ import (
"github.com/superplanehq/superplane/pkg/integrations/aws/lambda"
"github.com/superplanehq/superplane/pkg/integrations/aws/prometheus"
"github.com/superplanehq/superplane/pkg/integrations/aws/route53"
+ "github.com/superplanehq/superplane/pkg/integrations/aws/s3"
"github.com/superplanehq/superplane/pkg/integrations/aws/sns"
"github.com/superplanehq/superplane/pkg/integrations/aws/sqs"
)
@@ -103,6 +104,9 @@ func (a *AWS) ListResources(resourceType string, ctx core.ListResourcesContext)
case "sqs.queue":
return sqs.ListQueues(ctx, resourceType)
+ case "s3.bucket":
+ return s3.ListBuckets(ctx, resourceType)
+
case "prometheus.workspace":
return prometheus.ListWorkspaces(ctx, resourceType)
diff --git a/pkg/integrations/aws/s3/client.go b/pkg/integrations/aws/s3/client.go
new file mode 100644
index 0000000000..258af659c1
--- /dev/null
+++ b/pkg/integrations/aws/s3/client.go
@@ -0,0 +1,185 @@
+package s3
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
+ "github.com/superplanehq/superplane/pkg/core"
+)
+
+// emptyPayloadHash is the SHA-256 of an empty body, required when signing
+// S3 requests that carry no payload (GET/DELETE/HEAD).
+const emptyPayloadHash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+
+// defaultRegion is the region whose buckets report an empty LocationConstraint.
+const defaultRegion = "us-east-1"
+
+type Client struct {
+ http core.HTTPContext
+ region string
+ credentials *aws.Credentials
+ signer *v4.Signer
+}
+
+func NewClient(httpCtx core.HTTPContext, credentials *aws.Credentials, region string) *Client {
+ return &Client{
+ http: httpCtx,
+ region: strings.TrimSpace(region),
+ credentials: credentials,
+ signer: v4.NewSigner(),
+ }
+}
+
+type Bucket struct {
+ Name string
+ CreationDate string
+}
+
+type listAllMyBucketsResult struct {
+ XMLName xml.Name `xml:"ListAllMyBucketsResult"`
+ Buckets []bucketElement `xml:"Buckets>Bucket"`
+}
+
+type bucketElement struct {
+ Name string `xml:"Name"`
+ CreationDate string `xml:"CreationDate"`
+}
+
+type locationConstraint struct {
+ XMLName xml.Name `xml:"LocationConstraint"`
+ Value string `xml:",chardata"`
+}
+
+// bucketARN returns the canonical S3 bucket ARN. S3 bucket ARNs omit region
+// and account ID.
+func bucketARN(name string) string {
+ return fmt.Sprintf("arn:aws:s3:::%s", strings.TrimSpace(name))
+}
+
+// endpoint returns the regional path-style S3 endpoint.
+func (c *Client) endpoint() string {
+ return fmt.Sprintf("https://s3.%s.amazonaws.com", c.region)
+}
+
+func (c *Client) bucketURL(bucket string) string {
+ return fmt.Sprintf("%s/%s", c.endpoint(), url.PathEscape(strings.TrimSpace(bucket)))
+}
+
+// CreateBucket creates a new bucket in the client's region. Regions other than
+// us-east-1 require a CreateBucketConfiguration body with the LocationConstraint.
+func (c *Client) CreateBucket(name string) error {
+ var body []byte
+ if c.region != "" && c.region != defaultRegion {
+ body = []byte(fmt.Sprintf(
+ `%s`,
+ c.region,
+ ))
+ }
+
+ _, err := c.do(http.MethodPut, c.bucketURL(name), body)
+ return err
+}
+
+// GetBucketLocation returns the region a bucket resides in. An empty
+// LocationConstraint maps to us-east-1.
+func (c *Client) GetBucketLocation(name string) (string, error) {
+ body, err := c.do(http.MethodGet, c.bucketURL(name)+"?location=", nil)
+ if err != nil {
+ return "", err
+ }
+
+ var location locationConstraint
+ if err := xml.Unmarshal(body, &location); err != nil {
+ return "", fmt.Errorf("failed to decode GetBucketLocation response: %w", err)
+ }
+
+ region := strings.TrimSpace(location.Value)
+ if region == "" {
+ return defaultRegion, nil
+ }
+
+ return region, nil
+}
+
+// DeleteBucket deletes an (empty) bucket.
+func (c *Client) DeleteBucket(name string) error {
+ _, err := c.do(http.MethodDelete, c.bucketURL(name), nil)
+ return err
+}
+
+// ListBuckets returns every bucket owned by the authenticated account.
+func (c *Client) ListBuckets() ([]Bucket, error) {
+ body, err := c.do(http.MethodGet, c.endpoint()+"/", nil)
+ if err != nil {
+ return nil, err
+ }
+
+ var result listAllMyBucketsResult
+ if err := xml.Unmarshal(body, &result); err != nil {
+ return nil, fmt.Errorf("failed to decode ListBuckets response: %w", err)
+ }
+
+ buckets := make([]Bucket, 0, len(result.Buckets))
+ for _, b := range result.Buckets {
+ buckets = append(buckets, Bucket{
+ Name: b.Name,
+ CreationDate: b.CreationDate,
+ })
+ }
+
+ return buckets, nil
+}
+
+func (c *Client) do(method, requestURL string, body []byte) ([]byte, error) {
+ var reader io.Reader
+ if body != nil {
+ reader = bytes.NewReader(body)
+ }
+
+ req, err := http.NewRequest(method, requestURL, reader)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build S3 request: %w", err)
+ }
+
+ if err := c.signRequest(req, body); err != nil {
+ return nil, err
+ }
+
+ res, err := c.http.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("S3 request failed: %w", err)
+ }
+ defer res.Body.Close()
+
+ responseBody, err := io.ReadAll(res.Body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read S3 response: %w", err)
+ }
+
+ if res.StatusCode < 200 || res.StatusCode >= 300 {
+ return nil, fmt.Errorf("S3 API request failed with %d: %s", res.StatusCode, string(responseBody))
+ }
+
+ return responseBody, nil
+}
+
+func (c *Client) signRequest(req *http.Request, payload []byte) error {
+ payloadHash := emptyPayloadHash
+ if payload != nil {
+ hash := sha256.Sum256(payload)
+ payloadHash = hex.EncodeToString(hash[:])
+ }
+
+ return c.signer.SignHTTP(context.Background(), *c.credentials, req, payloadHash, "s3", c.region, time.Now())
+}
diff --git a/pkg/integrations/aws/s3/create_bucket.go b/pkg/integrations/aws/s3/create_bucket.go
new file mode 100644
index 0000000000..37502274b2
--- /dev/null
+++ b/pkg/integrations/aws/s3/create_bucket.go
@@ -0,0 +1,171 @@
+package s3
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/google/uuid"
+ "github.com/mitchellh/mapstructure"
+ "github.com/superplanehq/superplane/pkg/configuration"
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/pkg/integrations/aws/common"
+)
+
+type CreateBucket struct{}
+
+type CreateBucketConfiguration struct {
+ Region string `json:"region" mapstructure:"region"`
+ BucketName string `json:"bucketName" mapstructure:"bucketName"`
+}
+
+func (c *CreateBucket) Name() string {
+ return "aws.s3.createBucket"
+}
+
+func (c *CreateBucket) Label() string {
+ return "S3 • Create Bucket"
+}
+
+func (c *CreateBucket) Description() string {
+ return "Create a new S3 bucket"
+}
+
+func (c *CreateBucket) Documentation() string {
+ return `The Create Bucket component creates a new AWS S3 bucket.
+
+## Configuration
+
+- **Region**: AWS region where the bucket will be created
+- **Bucket Name**: Globally unique name for the new bucket (3-63 characters, lowercase)
+
+## Output
+
+Emits the created bucket's name, region, and ARN on the default channel.`
+}
+
+func (c *CreateBucket) Icon() string {
+ return "aws"
+}
+
+func (c *CreateBucket) Color() string {
+ return "gray"
+}
+
+func (c *CreateBucket) OutputChannels(configuration any) []core.OutputChannel {
+ return []core.OutputChannel{core.DefaultOutputChannel}
+}
+
+func (c *CreateBucket) Configuration() []configuration.Field {
+ return []configuration.Field{
+ {
+ Name: "region",
+ Label: "Region",
+ Type: configuration.FieldTypeSelect,
+ Required: true,
+ Default: "us-east-1",
+ TypeOptions: &configuration.TypeOptions{
+ Select: &configuration.SelectTypeOptions{
+ Options: common.AllRegions,
+ },
+ },
+ },
+ {
+ Name: "bucketName",
+ Label: "Bucket Name",
+ Type: configuration.FieldTypeString,
+ Required: true,
+ Description: "Globally unique name for the new bucket",
+ VisibilityConditions: []configuration.VisibilityCondition{
+ {
+ Field: "region",
+ Values: []string{"*"},
+ },
+ },
+ },
+ }
+}
+
+func (c *CreateBucket) Setup(ctx core.SetupContext) error {
+ var config CreateBucketConfiguration
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+
+ config.Region = strings.TrimSpace(config.Region)
+ config.BucketName = strings.TrimSpace(config.BucketName)
+
+ if config.Region == "" {
+ return fmt.Errorf("region is required")
+ }
+
+ if config.BucketName == "" {
+ return fmt.Errorf("bucket name is required")
+ }
+
+ return nil
+}
+
+func (c *CreateBucket) ProcessQueueItem(ctx core.ProcessQueueContext) (*uuid.UUID, error) {
+ return ctx.DefaultProcessing()
+}
+
+func (c *CreateBucket) Execute(ctx core.ExecutionContext) error {
+ var config CreateBucketConfiguration
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+
+ config.Region = strings.TrimSpace(config.Region)
+ config.BucketName = strings.TrimSpace(config.BucketName)
+
+ if config.Region == "" {
+ return fmt.Errorf("region is required")
+ }
+
+ if config.BucketName == "" {
+ return fmt.Errorf("bucket name is required")
+ }
+
+ creds, err := common.CredentialsFromInstallation(ctx.Integration)
+ if err != nil {
+ return fmt.Errorf("failed to get AWS credentials: %w", err)
+ }
+
+ client := NewClient(ctx.HTTP, creds, config.Region)
+ if err := client.CreateBucket(config.BucketName); err != nil {
+ return fmt.Errorf("failed to create S3 bucket: %w", err)
+ }
+
+ output := map[string]any{
+ "bucketName": config.BucketName,
+ "region": config.Region,
+ "arn": bucketARN(config.BucketName),
+ }
+
+ return ctx.ExecutionState.Emit(
+ core.DefaultOutputChannel.Name,
+ "aws.s3.bucket",
+ []any{output},
+ )
+}
+
+func (c *CreateBucket) HandleWebhook(ctx core.WebhookRequestContext) (int, *core.WebhookResponseBody, error) {
+ return http.StatusOK, nil, nil
+}
+
+func (c *CreateBucket) Cancel(ctx core.ExecutionContext) error {
+ return nil
+}
+
+func (c *CreateBucket) Cleanup(ctx core.SetupContext) error {
+ return nil
+}
+
+func (c *CreateBucket) Hooks() []core.Hook {
+ return []core.Hook{}
+}
+
+func (c *CreateBucket) HandleHook(ctx core.ActionHookContext) error {
+ return nil
+}
diff --git a/pkg/integrations/aws/s3/create_bucket_test.go b/pkg/integrations/aws/s3/create_bucket_test.go
new file mode 100644
index 0000000000..bf56609b4f
--- /dev/null
+++ b/pkg/integrations/aws/s3/create_bucket_test.go
@@ -0,0 +1,139 @@
+package s3
+
+import (
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/test/support/contexts"
+)
+
+func awsSecrets() map[string]core.IntegrationSecret {
+ return map[string]core.IntegrationSecret{
+ "accessKeyId": {Name: "accessKeyId", Value: []byte("key")},
+ "secretAccessKey": {Name: "secretAccessKey", Value: []byte("secret")},
+ "sessionToken": {Name: "sessionToken", Value: []byte("token")},
+ }
+}
+
+func Test__CreateBucket__Setup(t *testing.T) {
+ component := &CreateBucket{}
+
+ t.Run("invalid configuration -> error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{Configuration: "invalid"})
+ require.ErrorContains(t, err, "failed to decode configuration")
+ })
+
+ t.Run("missing region -> error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{"region": " ", "bucketName": "my-bucket"},
+ })
+ require.ErrorContains(t, err, "region is required")
+ })
+
+ t.Run("missing bucket name -> error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucketName": " "},
+ })
+ require.ErrorContains(t, err, "bucket name is required")
+ })
+
+ t.Run("valid configuration -> ok", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucketName": "my-bucket"},
+ })
+ require.NoError(t, err)
+ })
+}
+
+func Test__CreateBucket__Execute(t *testing.T) {
+ component := &CreateBucket{}
+
+ t.Run("invalid configuration -> error", func(t *testing.T) {
+ err := component.Execute(core.ExecutionContext{
+ Configuration: "invalid",
+ ExecutionState: &contexts.ExecutionStateContext{KVs: map[string]string{}},
+ })
+ require.ErrorContains(t, err, "failed to decode configuration")
+ })
+
+ t.Run("missing credentials -> error", func(t *testing.T) {
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucketName": "my-bucket"},
+ Integration: &contexts.IntegrationContext{},
+ ExecutionState: &contexts.ExecutionStateContext{KVs: map[string]string{}},
+ })
+ require.ErrorContains(t, err, "AWS session credentials are missing")
+ })
+
+ t.Run("us-east-1 -> creates bucket without location constraint", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{{StatusCode: http.StatusOK, Body: http.NoBody}},
+ }
+ execState := &contexts.ExecutionStateContext{KVs: map[string]string{}}
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucketName": "my-bucket"},
+ HTTP: httpContext,
+ ExecutionState: execState,
+ Integration: &contexts.IntegrationContext{CurrentSecrets: awsSecrets()},
+ })
+
+ require.NoError(t, err)
+ assert.True(t, execState.Passed)
+ assert.Equal(t, "aws.s3.bucket", execState.Type)
+
+ require.Len(t, execState.Payloads, 1)
+ data := execState.Payloads[0].(map[string]any)["data"].(map[string]any)
+ assert.Equal(t, "my-bucket", data["bucketName"])
+ assert.Equal(t, "us-east-1", data["region"])
+ assert.Equal(t, "arn:aws:s3:::my-bucket", data["arn"])
+
+ require.Len(t, httpContext.Requests, 1)
+ assert.Equal(t, http.MethodPut, httpContext.Requests[0].Method)
+ assert.Equal(t, "https://s3.us-east-1.amazonaws.com/my-bucket", httpContext.Requests[0].URL.String())
+ })
+
+ t.Run("non us-east-1 -> sends location constraint", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{{StatusCode: http.StatusOK, Body: http.NoBody}},
+ }
+ execState := &contexts.ExecutionStateContext{KVs: map[string]string{}}
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{"region": "eu-west-1", "bucketName": "my-bucket"},
+ HTTP: httpContext,
+ ExecutionState: execState,
+ Integration: &contexts.IntegrationContext{CurrentSecrets: awsSecrets()},
+ })
+
+ require.NoError(t, err)
+ require.Len(t, httpContext.Requests, 1)
+ bodyBytes, _ := io.ReadAll(httpContext.Requests[0].Body)
+ assert.Contains(t, string(bodyBytes), "eu-west-1")
+ })
+
+ t.Run("API error -> error", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{{
+ StatusCode: http.StatusConflict,
+ Body: io.NopCloser(strings.NewReader(`BucketAlreadyExists`)),
+ }},
+ }
+ execState := &contexts.ExecutionStateContext{KVs: map[string]string{}}
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucketName": "my-bucket"},
+ HTTP: httpContext,
+ ExecutionState: execState,
+ Integration: &contexts.IntegrationContext{CurrentSecrets: awsSecrets()},
+ })
+
+ require.ErrorContains(t, err, "failed to create S3 bucket")
+ assert.False(t, execState.Passed)
+ })
+}
diff --git a/pkg/integrations/aws/s3/delete_bucket.go b/pkg/integrations/aws/s3/delete_bucket.go
new file mode 100644
index 0000000000..35b3958e12
--- /dev/null
+++ b/pkg/integrations/aws/s3/delete_bucket.go
@@ -0,0 +1,183 @@
+package s3
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/google/uuid"
+ "github.com/mitchellh/mapstructure"
+ "github.com/superplanehq/superplane/pkg/configuration"
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/pkg/integrations/aws/common"
+)
+
+type DeleteBucket struct{}
+
+type DeleteBucketConfiguration struct {
+ Region string `json:"region" mapstructure:"region"`
+ Bucket string `json:"bucket" mapstructure:"bucket"`
+}
+
+func (c *DeleteBucket) Name() string {
+ return "aws.s3.deleteBucket"
+}
+
+func (c *DeleteBucket) Label() string {
+ return "S3 • Delete Bucket"
+}
+
+func (c *DeleteBucket) Description() string {
+ return "Delete an S3 bucket"
+}
+
+func (c *DeleteBucket) Documentation() string {
+ return `The Delete Bucket component deletes an AWS S3 bucket. The bucket must be empty.
+
+## Configuration
+
+- **Region**: AWS region of the S3 bucket
+- **Bucket**: Target S3 bucket to delete
+
+## Output
+
+Emits the deleted bucket's name and a deletion confirmation on the default channel.`
+}
+
+func (c *DeleteBucket) Icon() string {
+ return "aws"
+}
+
+func (c *DeleteBucket) Color() string {
+ return "gray"
+}
+
+func (c *DeleteBucket) OutputChannels(configuration any) []core.OutputChannel {
+ return []core.OutputChannel{core.DefaultOutputChannel}
+}
+
+func (c *DeleteBucket) Configuration() []configuration.Field {
+ return []configuration.Field{
+ {
+ Name: "region",
+ Label: "Region",
+ Type: configuration.FieldTypeSelect,
+ Required: true,
+ Default: "us-east-1",
+ TypeOptions: &configuration.TypeOptions{
+ Select: &configuration.SelectTypeOptions{
+ Options: common.AllRegions,
+ },
+ },
+ },
+ {
+ Name: "bucket",
+ Label: "Bucket",
+ Type: configuration.FieldTypeIntegrationResource,
+ Required: true,
+ Description: "Target S3 bucket to delete",
+ VisibilityConditions: []configuration.VisibilityCondition{
+ {
+ Field: "region",
+ Values: []string{"*"},
+ },
+ },
+ TypeOptions: &configuration.TypeOptions{
+ Resource: &configuration.ResourceTypeOptions{
+ Type: "s3.bucket",
+ Parameters: []configuration.ParameterRef{
+ {
+ Name: "region",
+ ValueFrom: &configuration.ParameterValueFrom{
+ Field: "region",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (c *DeleteBucket) Setup(ctx core.SetupContext) error {
+ var config DeleteBucketConfiguration
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+
+ config.Region = strings.TrimSpace(config.Region)
+ config.Bucket = strings.TrimSpace(config.Bucket)
+
+ if config.Region == "" {
+ return fmt.Errorf("region is required")
+ }
+
+ if config.Bucket == "" {
+ return fmt.Errorf("bucket is required")
+ }
+
+ return nil
+}
+
+func (c *DeleteBucket) ProcessQueueItem(ctx core.ProcessQueueContext) (*uuid.UUID, error) {
+ return ctx.DefaultProcessing()
+}
+
+func (c *DeleteBucket) Execute(ctx core.ExecutionContext) error {
+ var config DeleteBucketConfiguration
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+
+ config.Region = strings.TrimSpace(config.Region)
+ config.Bucket = strings.TrimSpace(config.Bucket)
+
+ if config.Region == "" {
+ return fmt.Errorf("region is required")
+ }
+
+ if config.Bucket == "" {
+ return fmt.Errorf("bucket is required")
+ }
+
+ creds, err := common.CredentialsFromInstallation(ctx.Integration)
+ if err != nil {
+ return fmt.Errorf("failed to get AWS credentials: %w", err)
+ }
+
+ client := NewClient(ctx.HTTP, creds, config.Region)
+ if err := client.DeleteBucket(config.Bucket); err != nil {
+ return fmt.Errorf("failed to delete S3 bucket: %w", err)
+ }
+
+ output := map[string]any{
+ "bucketName": config.Bucket,
+ "deleted": true,
+ }
+
+ return ctx.ExecutionState.Emit(
+ core.DefaultOutputChannel.Name,
+ "aws.s3.bucket.deleted",
+ []any{output},
+ )
+}
+
+func (c *DeleteBucket) HandleWebhook(ctx core.WebhookRequestContext) (int, *core.WebhookResponseBody, error) {
+ return http.StatusOK, nil, nil
+}
+
+func (c *DeleteBucket) Cancel(ctx core.ExecutionContext) error {
+ return nil
+}
+
+func (c *DeleteBucket) Cleanup(ctx core.SetupContext) error {
+ return nil
+}
+
+func (c *DeleteBucket) Hooks() []core.Hook {
+ return []core.Hook{}
+}
+
+func (c *DeleteBucket) HandleHook(ctx core.ActionHookContext) error {
+ return nil
+}
diff --git a/pkg/integrations/aws/s3/delete_bucket_test.go b/pkg/integrations/aws/s3/delete_bucket_test.go
new file mode 100644
index 0000000000..b8e673cf8b
--- /dev/null
+++ b/pkg/integrations/aws/s3/delete_bucket_test.go
@@ -0,0 +1,103 @@
+package s3
+
+import (
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/test/support/contexts"
+)
+
+func Test__DeleteBucket__Setup(t *testing.T) {
+ component := &DeleteBucket{}
+
+ t.Run("invalid configuration -> error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{Configuration: "invalid"})
+ require.ErrorContains(t, err, "failed to decode configuration")
+ })
+
+ t.Run("missing region -> error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{"region": " ", "bucket": "my-bucket"},
+ })
+ require.ErrorContains(t, err, "region is required")
+ })
+
+ t.Run("missing bucket -> error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucket": " "},
+ })
+ require.ErrorContains(t, err, "bucket is required")
+ })
+
+ t.Run("valid configuration -> ok", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucket": "my-bucket"},
+ })
+ require.NoError(t, err)
+ })
+}
+
+func Test__DeleteBucket__Execute(t *testing.T) {
+ component := &DeleteBucket{}
+
+ t.Run("missing credentials -> error", func(t *testing.T) {
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucket": "my-bucket"},
+ Integration: &contexts.IntegrationContext{},
+ ExecutionState: &contexts.ExecutionStateContext{KVs: map[string]string{}},
+ })
+ require.ErrorContains(t, err, "AWS session credentials are missing")
+ })
+
+ t.Run("valid request -> emits deletion confirmation", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{{StatusCode: http.StatusNoContent, Body: http.NoBody}},
+ }
+ execState := &contexts.ExecutionStateContext{KVs: map[string]string{}}
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucket": "my-bucket"},
+ HTTP: httpContext,
+ ExecutionState: execState,
+ Integration: &contexts.IntegrationContext{CurrentSecrets: awsSecrets()},
+ })
+
+ require.NoError(t, err)
+ assert.True(t, execState.Passed)
+ assert.Equal(t, "aws.s3.bucket.deleted", execState.Type)
+
+ require.Len(t, execState.Payloads, 1)
+ data := execState.Payloads[0].(map[string]any)["data"].(map[string]any)
+ assert.Equal(t, "my-bucket", data["bucketName"])
+ assert.Equal(t, true, data["deleted"])
+
+ require.Len(t, httpContext.Requests, 1)
+ assert.Equal(t, http.MethodDelete, httpContext.Requests[0].Method)
+ assert.Equal(t, "https://s3.us-east-1.amazonaws.com/my-bucket", httpContext.Requests[0].URL.String())
+ })
+
+ t.Run("API error -> error", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{{
+ StatusCode: http.StatusConflict,
+ Body: io.NopCloser(strings.NewReader(`BucketNotEmpty`)),
+ }},
+ }
+ execState := &contexts.ExecutionStateContext{KVs: map[string]string{}}
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucket": "my-bucket"},
+ HTTP: httpContext,
+ ExecutionState: execState,
+ Integration: &contexts.IntegrationContext{CurrentSecrets: awsSecrets()},
+ })
+
+ require.ErrorContains(t, err, "failed to delete S3 bucket")
+ assert.False(t, execState.Passed)
+ })
+}
diff --git a/pkg/integrations/aws/s3/example.go b/pkg/integrations/aws/s3/example.go
new file mode 100644
index 0000000000..d8511b33b6
--- /dev/null
+++ b/pkg/integrations/aws/s3/example.go
@@ -0,0 +1,38 @@
+package s3
+
+import (
+ _ "embed"
+ "sync"
+
+ "github.com/superplanehq/superplane/pkg/utils"
+)
+
+//go:embed example_output_create_bucket.json
+var exampleOutputCreateBucketBytes []byte
+
+var exampleOutputCreateBucketOnce sync.Once
+var exampleOutputCreateBucket map[string]any
+
+//go:embed example_output_get_bucket.json
+var exampleOutputGetBucketBytes []byte
+
+var exampleOutputGetBucketOnce sync.Once
+var exampleOutputGetBucket map[string]any
+
+//go:embed example_output_delete_bucket.json
+var exampleOutputDeleteBucketBytes []byte
+
+var exampleOutputDeleteBucketOnce sync.Once
+var exampleOutputDeleteBucket map[string]any
+
+func (c *CreateBucket) ExampleOutput() map[string]any {
+ return utils.UnmarshalEmbeddedJSON(&exampleOutputCreateBucketOnce, exampleOutputCreateBucketBytes, &exampleOutputCreateBucket)
+}
+
+func (c *GetBucket) ExampleOutput() map[string]any {
+ return utils.UnmarshalEmbeddedJSON(&exampleOutputGetBucketOnce, exampleOutputGetBucketBytes, &exampleOutputGetBucket)
+}
+
+func (c *DeleteBucket) ExampleOutput() map[string]any {
+ return utils.UnmarshalEmbeddedJSON(&exampleOutputDeleteBucketOnce, exampleOutputDeleteBucketBytes, &exampleOutputDeleteBucket)
+}
diff --git a/pkg/integrations/aws/s3/example_output_create_bucket.json b/pkg/integrations/aws/s3/example_output_create_bucket.json
new file mode 100644
index 0000000000..aa59387d88
--- /dev/null
+++ b/pkg/integrations/aws/s3/example_output_create_bucket.json
@@ -0,0 +1,9 @@
+{
+ "type": "aws.s3.bucket",
+ "timestamp": "2026-02-11T12:00:00Z",
+ "data": {
+ "bucketName": "my-example-bucket",
+ "region": "us-east-1",
+ "arn": "arn:aws:s3:::my-example-bucket"
+ }
+}
diff --git a/pkg/integrations/aws/s3/example_output_delete_bucket.json b/pkg/integrations/aws/s3/example_output_delete_bucket.json
new file mode 100644
index 0000000000..82d93b3266
--- /dev/null
+++ b/pkg/integrations/aws/s3/example_output_delete_bucket.json
@@ -0,0 +1,8 @@
+{
+ "type": "aws.s3.bucket.deleted",
+ "timestamp": "2026-02-11T12:00:00Z",
+ "data": {
+ "bucketName": "my-example-bucket",
+ "deleted": true
+ }
+}
diff --git a/pkg/integrations/aws/s3/example_output_get_bucket.json b/pkg/integrations/aws/s3/example_output_get_bucket.json
new file mode 100644
index 0000000000..45962eb333
--- /dev/null
+++ b/pkg/integrations/aws/s3/example_output_get_bucket.json
@@ -0,0 +1,9 @@
+{
+ "type": "aws.s3.bucket",
+ "timestamp": "2026-02-11T12:00:00Z",
+ "data": {
+ "bucketName": "my-example-bucket",
+ "region": "eu-west-1",
+ "arn": "arn:aws:s3:::my-example-bucket"
+ }
+}
diff --git a/pkg/integrations/aws/s3/get_bucket.go b/pkg/integrations/aws/s3/get_bucket.go
new file mode 100644
index 0000000000..774771bb9b
--- /dev/null
+++ b/pkg/integrations/aws/s3/get_bucket.go
@@ -0,0 +1,185 @@
+package s3
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+
+ "github.com/google/uuid"
+ "github.com/mitchellh/mapstructure"
+ "github.com/superplanehq/superplane/pkg/configuration"
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/pkg/integrations/aws/common"
+)
+
+type GetBucket struct{}
+
+type GetBucketConfiguration struct {
+ Region string `json:"region" mapstructure:"region"`
+ Bucket string `json:"bucket" mapstructure:"bucket"`
+}
+
+func (c *GetBucket) Name() string {
+ return "aws.s3.getBucket"
+}
+
+func (c *GetBucket) Label() string {
+ return "S3 • Get Bucket"
+}
+
+func (c *GetBucket) Description() string {
+ return "Get metadata for an S3 bucket"
+}
+
+func (c *GetBucket) Documentation() string {
+ return `The Get Bucket component retrieves metadata for an AWS S3 bucket.
+
+## Configuration
+
+- **Region**: AWS region of the S3 bucket
+- **Bucket**: Target S3 bucket
+
+## Output
+
+Emits the bucket's name, resolved region, and ARN on the default channel.`
+}
+
+func (c *GetBucket) Icon() string {
+ return "aws"
+}
+
+func (c *GetBucket) Color() string {
+ return "gray"
+}
+
+func (c *GetBucket) OutputChannels(configuration any) []core.OutputChannel {
+ return []core.OutputChannel{core.DefaultOutputChannel}
+}
+
+func (c *GetBucket) Configuration() []configuration.Field {
+ return []configuration.Field{
+ {
+ Name: "region",
+ Label: "Region",
+ Type: configuration.FieldTypeSelect,
+ Required: true,
+ Default: "us-east-1",
+ TypeOptions: &configuration.TypeOptions{
+ Select: &configuration.SelectTypeOptions{
+ Options: common.AllRegions,
+ },
+ },
+ },
+ {
+ Name: "bucket",
+ Label: "Bucket",
+ Type: configuration.FieldTypeIntegrationResource,
+ Required: true,
+ Description: "Target S3 bucket",
+ VisibilityConditions: []configuration.VisibilityCondition{
+ {
+ Field: "region",
+ Values: []string{"*"},
+ },
+ },
+ TypeOptions: &configuration.TypeOptions{
+ Resource: &configuration.ResourceTypeOptions{
+ Type: "s3.bucket",
+ Parameters: []configuration.ParameterRef{
+ {
+ Name: "region",
+ ValueFrom: &configuration.ParameterValueFrom{
+ Field: "region",
+ },
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+func (c *GetBucket) Setup(ctx core.SetupContext) error {
+ var config GetBucketConfiguration
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+
+ config.Region = strings.TrimSpace(config.Region)
+ config.Bucket = strings.TrimSpace(config.Bucket)
+
+ if config.Region == "" {
+ return fmt.Errorf("region is required")
+ }
+
+ if config.Bucket == "" {
+ return fmt.Errorf("bucket is required")
+ }
+
+ return nil
+}
+
+func (c *GetBucket) ProcessQueueItem(ctx core.ProcessQueueContext) (*uuid.UUID, error) {
+ return ctx.DefaultProcessing()
+}
+
+func (c *GetBucket) Execute(ctx core.ExecutionContext) error {
+ var config GetBucketConfiguration
+ if err := mapstructure.Decode(ctx.Configuration, &config); err != nil {
+ return fmt.Errorf("failed to decode configuration: %w", err)
+ }
+
+ config.Region = strings.TrimSpace(config.Region)
+ config.Bucket = strings.TrimSpace(config.Bucket)
+
+ if config.Region == "" {
+ return fmt.Errorf("region is required")
+ }
+
+ if config.Bucket == "" {
+ return fmt.Errorf("bucket is required")
+ }
+
+ creds, err := common.CredentialsFromInstallation(ctx.Integration)
+ if err != nil {
+ return fmt.Errorf("failed to get AWS credentials: %w", err)
+ }
+
+ client := NewClient(ctx.HTTP, creds, config.Region)
+ location, err := client.GetBucketLocation(config.Bucket)
+ if err != nil {
+ return fmt.Errorf("failed to get S3 bucket: %w", err)
+ }
+
+ output := map[string]any{
+ "bucketName": config.Bucket,
+ "region": location,
+ "arn": bucketARN(config.Bucket),
+ }
+
+ return ctx.ExecutionState.Emit(
+ core.DefaultOutputChannel.Name,
+ "aws.s3.bucket",
+ []any{output},
+ )
+}
+
+func (c *GetBucket) HandleWebhook(ctx core.WebhookRequestContext) (int, *core.WebhookResponseBody, error) {
+ return http.StatusOK, nil, nil
+}
+
+func (c *GetBucket) Cancel(ctx core.ExecutionContext) error {
+ return nil
+}
+
+func (c *GetBucket) Cleanup(ctx core.SetupContext) error {
+ return nil
+}
+
+func (c *GetBucket) Hooks() []core.Hook {
+ return []core.Hook{}
+}
+
+func (c *GetBucket) HandleHook(ctx core.ActionHookContext) error {
+ return nil
+}
diff --git a/pkg/integrations/aws/s3/get_bucket_test.go b/pkg/integrations/aws/s3/get_bucket_test.go
new file mode 100644
index 0000000000..bd798c0d31
--- /dev/null
+++ b/pkg/integrations/aws/s3/get_bucket_test.go
@@ -0,0 +1,132 @@
+package s3
+
+import (
+ "io"
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/test/support/contexts"
+)
+
+func Test__GetBucket__Setup(t *testing.T) {
+ component := &GetBucket{}
+
+ t.Run("invalid configuration -> error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{Configuration: "invalid"})
+ require.ErrorContains(t, err, "failed to decode configuration")
+ })
+
+ t.Run("missing region -> error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{"region": " ", "bucket": "my-bucket"},
+ })
+ require.ErrorContains(t, err, "region is required")
+ })
+
+ t.Run("missing bucket -> error", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucket": " "},
+ })
+ require.ErrorContains(t, err, "bucket is required")
+ })
+
+ t.Run("valid configuration -> ok", func(t *testing.T) {
+ err := component.Setup(core.SetupContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucket": "my-bucket"},
+ })
+ require.NoError(t, err)
+ })
+}
+
+func Test__GetBucket__Execute(t *testing.T) {
+ component := &GetBucket{}
+
+ t.Run("missing credentials -> error", func(t *testing.T) {
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucket": "my-bucket"},
+ Integration: &contexts.IntegrationContext{},
+ ExecutionState: &contexts.ExecutionStateContext{KVs: map[string]string{}},
+ })
+ require.ErrorContains(t, err, "AWS session credentials are missing")
+ })
+
+ t.Run("valid request -> emits resolved region", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{{
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(strings.NewReader(
+ `eu-west-1`,
+ )),
+ }},
+ }
+ execState := &contexts.ExecutionStateContext{KVs: map[string]string{}}
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{"region": "eu-west-1", "bucket": "my-bucket"},
+ HTTP: httpContext,
+ ExecutionState: execState,
+ Integration: &contexts.IntegrationContext{CurrentSecrets: awsSecrets()},
+ })
+
+ require.NoError(t, err)
+ assert.True(t, execState.Passed)
+ assert.Equal(t, "aws.s3.bucket", execState.Type)
+
+ require.Len(t, execState.Payloads, 1)
+ data := execState.Payloads[0].(map[string]any)["data"].(map[string]any)
+ assert.Equal(t, "my-bucket", data["bucketName"])
+ assert.Equal(t, "eu-west-1", data["region"])
+ assert.Equal(t, "arn:aws:s3:::my-bucket", data["arn"])
+
+ require.Len(t, httpContext.Requests, 1)
+ assert.Equal(t, http.MethodGet, httpContext.Requests[0].Method)
+ assert.Contains(t, httpContext.Requests[0].URL.String(), "location")
+ })
+
+ t.Run("empty location constraint -> defaults to us-east-1", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{{
+ StatusCode: http.StatusOK,
+ Body: io.NopCloser(strings.NewReader(
+ ``,
+ )),
+ }},
+ }
+ execState := &contexts.ExecutionStateContext{KVs: map[string]string{}}
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucket": "my-bucket"},
+ HTTP: httpContext,
+ ExecutionState: execState,
+ Integration: &contexts.IntegrationContext{CurrentSecrets: awsSecrets()},
+ })
+
+ require.NoError(t, err)
+ data := execState.Payloads[0].(map[string]any)["data"].(map[string]any)
+ assert.Equal(t, "us-east-1", data["region"])
+ })
+
+ t.Run("API error -> error", func(t *testing.T) {
+ httpContext := &contexts.HTTPContext{
+ Responses: []*http.Response{{
+ StatusCode: http.StatusNotFound,
+ Body: io.NopCloser(strings.NewReader(`NoSuchBucket`)),
+ }},
+ }
+ execState := &contexts.ExecutionStateContext{KVs: map[string]string{}}
+
+ err := component.Execute(core.ExecutionContext{
+ Configuration: map[string]any{"region": "us-east-1", "bucket": "my-bucket"},
+ HTTP: httpContext,
+ ExecutionState: execState,
+ Integration: &contexts.IntegrationContext{CurrentSecrets: awsSecrets()},
+ })
+
+ require.ErrorContains(t, err, "failed to get S3 bucket")
+ assert.False(t, execState.Passed)
+ })
+}
diff --git a/pkg/integrations/aws/s3/resources.go b/pkg/integrations/aws/s3/resources.go
new file mode 100644
index 0000000000..205a171572
--- /dev/null
+++ b/pkg/integrations/aws/s3/resources.go
@@ -0,0 +1,37 @@
+package s3
+
+import (
+ "fmt"
+
+ "github.com/superplanehq/superplane/pkg/core"
+ "github.com/superplanehq/superplane/pkg/integrations/aws/common"
+)
+
+func ListBuckets(ctx core.ListResourcesContext, resourceType string) ([]core.IntegrationResource, error) {
+ credentials, err := common.CredentialsFromInstallation(ctx.Integration)
+ if err != nil {
+ return nil, err
+ }
+
+ region := ctx.Parameters["region"]
+ if region == "" {
+ return nil, fmt.Errorf("region is required")
+ }
+
+ client := NewClient(ctx.HTTP, credentials, region)
+ buckets, err := client.ListBuckets()
+ if err != nil {
+ return nil, fmt.Errorf("failed to list S3 buckets: %w", err)
+ }
+
+ resources := make([]core.IntegrationResource, 0, len(buckets))
+ for _, bucket := range buckets {
+ resources = append(resources, core.IntegrationResource{
+ Type: resourceType,
+ Name: bucket.Name,
+ ID: bucket.Name,
+ })
+ }
+
+ return resources, nil
+}
diff --git a/web_src/src/pages/app/mappers/aws/index.ts b/web_src/src/pages/app/mappers/aws/index.ts
index cd5448f759..85c56e290a 100644
--- a/web_src/src/pages/app/mappers/aws/index.ts
+++ b/web_src/src/pages/app/mappers/aws/index.ts
@@ -9,6 +9,7 @@ import { scanImageMapper } from "./ecr/scan_image";
import { onPackageVersionTriggerRenderer } from "./codeartifact/on_package_version";
import { getPackageVersionMapper } from "./codeartifact/get_package_version";
import { createQueueMapper, deleteQueueMapper, getQueueMapper, purgeQueueMapper, sendMessageMapper } from "./sqs";
+import { createBucketMapper, deleteBucketMapper, getBucketMapper } from "./s3";
import { createRepositoryMapper } from "./codeartifact/create_repository";
import { copyPackageVersionsMapper } from "./codeartifact/copy_package_versions";
import { deletePackageVersionsMapper } from "./codeartifact/delete_package_versions";
@@ -100,6 +101,9 @@ export const componentMappers: Record = {
"sqs.sendMessage": sendMessageMapper,
"sqs.deleteQueue": deleteQueueMapper,
"sqs.purgeQueue": purgeQueueMapper,
+ "s3.createBucket": createBucketMapper,
+ "s3.getBucket": getBucketMapper,
+ "s3.deleteBucket": deleteBucketMapper,
"codeArtifact.updatePackageVersionsStatus": updatePackageVersionsStatusMapper,
"route53.createRecord": createRecordMapper,
"route53.upsertRecord": upsertRecordMapper,
@@ -174,6 +178,9 @@ export const eventStateRegistry: Record = {
"sqs.sendMessage": buildActionStateRegistry("sent"),
"sqs.deleteQueue": buildActionStateRegistry("deleted"),
"sqs.purgeQueue": buildActionStateRegistry("purged"),
+ "s3.createBucket": buildActionStateRegistry("created"),
+ "s3.getBucket": buildActionStateRegistry("retrieved"),
+ "s3.deleteBucket": buildActionStateRegistry("deleted"),
"codeArtifact.updatePackageVersionsStatus": buildActionStateRegistry("updated"),
"route53.createRecord": buildActionStateRegistry("created"),
"route53.upsertRecord": buildActionStateRegistry("upserted"),
diff --git a/web_src/src/pages/app/mappers/aws/s3/create_bucket.spec.ts b/web_src/src/pages/app/mappers/aws/s3/create_bucket.spec.ts
new file mode 100644
index 0000000000..5b67d3e58f
--- /dev/null
+++ b/web_src/src/pages/app/mappers/aws/s3/create_bucket.spec.ts
@@ -0,0 +1,110 @@
+import { describe, expect, it } from "vitest";
+
+import type {
+ ComponentBaseContext,
+ ComponentDefinition,
+ ExecutionDetailsContext,
+ ExecutionInfo,
+ NodeInfo,
+ OutputPayload,
+} from "../../types";
+import { createBucketMapper } from "./create_bucket";
+import { eventStateRegistry } from "../index";
+
+function buildNode(overrides?: Partial): NodeInfo {
+ return {
+ id: "node-1",
+ name: "Create Bucket Node",
+ componentName: "aws.s3.createBucket",
+ isCollapsed: false,
+ configuration: {},
+ metadata: {},
+ ...overrides,
+ };
+}
+
+function buildOutput(data: unknown): OutputPayload {
+ return { type: "aws.s3.bucket", timestamp: new Date().toISOString(), data };
+}
+
+function buildExecution(overrides?: Partial): ExecutionInfo {
+ return {
+ id: "exec-1",
+ createdAt: "2026-06-01T10:00:00.000Z",
+ updatedAt: "2026-06-01T10:00:05.000Z",
+ state: "STATE_FINISHED",
+ result: "RESULT_PASSED",
+ resultReason: "RESULT_REASON_OK",
+ resultMessage: "",
+ metadata: {},
+ configuration: {},
+ rootEvent: undefined,
+ ...overrides,
+ };
+}
+
+function buildDetailsCtx(overrides?: { node?: Partial; execution?: Partial }): ExecutionDetailsContext {
+ const node = buildNode(overrides?.node);
+ return { nodes: [node], node, execution: buildExecution(overrides?.execution) };
+}
+
+const defaultDefinition: ComponentDefinition = {
+ name: "aws.s3.createBucket",
+ label: "S3 • Create Bucket",
+ description: "",
+ icon: "aws",
+ color: "gray",
+};
+
+function buildPropsContext(overrides?: Partial): ComponentBaseContext {
+ return {
+ nodes: [],
+ node: buildNode(),
+ componentDefinition: defaultDefinition,
+ lastExecutions: [],
+ currentUser: undefined,
+ actions: { invokeNodeExecutionHook: async () => {} },
+ ...overrides,
+ };
+}
+
+const bucketOutput = {
+ bucketName: "my-example-bucket",
+ region: "us-east-1",
+ arn: "arn:aws:s3:::my-example-bucket",
+};
+
+describe("createBucketMapper.getExecutionDetails", () => {
+ it("does not throw when outputs is undefined", () => {
+ const ctx = buildDetailsCtx({ execution: { outputs: undefined } });
+ expect(() => createBucketMapper.getExecutionDetails(ctx)).not.toThrow();
+ });
+
+ it("extracts bucket fields from output", () => {
+ const ctx = buildDetailsCtx({ execution: { outputs: { default: [buildOutput(bucketOutput)] } } });
+ const details = createBucketMapper.getExecutionDetails(ctx);
+ expect(details["Bucket"]).toBe("my-example-bucket");
+ expect(details["Region"]).toBe("us-east-1");
+ expect(details["ARN"]).toBe("arn:aws:s3:::my-example-bucket");
+ });
+});
+
+describe("createBucketMapper.props", () => {
+ it("uses node name as title", () => {
+ expect(createBucketMapper.props(buildPropsContext()).title).toBe("Create Bucket Node");
+ });
+
+ it("includes bucket name from configuration in metadata", () => {
+ const props = createBucketMapper.props(
+ buildPropsContext({ node: buildNode({ configuration: { bucketName: "my-example-bucket", region: "us-east-1" } }) }),
+ );
+ const labels = props.metadata?.map((m) => m.label) ?? [];
+ expect(labels).toContain("my-example-bucket");
+ });
+});
+
+describe("eventStateRegistry['s3.createBucket']", () => {
+ it("maps finished success to created", () => {
+ expect(eventStateRegistry["s3.createBucket"].getState(buildExecution())).toBe("created");
+ });
+});
diff --git a/web_src/src/pages/app/mappers/aws/s3/create_bucket.ts b/web_src/src/pages/app/mappers/aws/s3/create_bucket.ts
new file mode 100644
index 0000000000..5a8d9f317d
--- /dev/null
+++ b/web_src/src/pages/app/mappers/aws/s3/create_bucket.ts
@@ -0,0 +1,101 @@
+import type {
+ ComponentBaseContext,
+ ComponentBaseMapper,
+ ExecutionDetailsContext,
+ ExecutionInfo,
+ NodeInfo,
+ OutputPayload,
+ SubtitleContext,
+} from "../../types";
+import type { ComponentBaseProps, EventSection } from "@/ui/componentBase";
+import type React from "react";
+import { getBackgroundColorClass, getColorClass } from "@/lib/colors";
+import { getState, getStateMap, getTriggerRenderer } from "../..";
+import awsIcon from "@/assets/icons/integrations/aws.svg";
+import { renderTimeAgo } from "@/components/TimeAgo";
+import type { MetadataItem } from "@/ui/metadataList";
+import { stringOrDash } from "../../utils";
+
+interface CreateBucketConfiguration {
+ region?: string;
+ bucketName?: string;
+}
+
+interface CreateBucketOutput {
+ bucketName?: string;
+ region?: string;
+ arn?: string;
+}
+
+export const createBucketMapper: ComponentBaseMapper = {
+ props(context: ComponentBaseContext): ComponentBaseProps {
+ const lastExecution = context.lastExecutions.length > 0 ? context.lastExecutions[0] : null;
+ const componentName = context.componentDefinition.name || "unknown";
+
+ return {
+ title:
+ context.node.name ||
+ context.componentDefinition.label ||
+ context.componentDefinition.name ||
+ "Unnamed component",
+ iconSrc: awsIcon,
+ iconColor: getColorClass(context.componentDefinition.color),
+ collapsedBackground: getBackgroundColorClass(context.componentDefinition.color),
+ collapsed: context.node.isCollapsed,
+ eventSections: lastExecution ? createBucketEventSections(context.nodes, lastExecution, componentName) : undefined,
+ includeEmptyState: !lastExecution,
+ metadata: createBucketMetadataList(context.node),
+ eventStateMap: getStateMap(componentName),
+ };
+ },
+
+ getExecutionDetails(context: ExecutionDetailsContext): Record {
+ const outputs = context.execution.outputs as { default?: OutputPayload[] } | undefined;
+ const result = outputs?.default?.[0]?.data as CreateBucketOutput | undefined;
+
+ if (!result) {
+ return {};
+ }
+
+ return {
+ Bucket: stringOrDash(result.bucketName),
+ Region: stringOrDash(result.region),
+ ARN: stringOrDash(result.arn),
+ };
+ },
+
+ subtitle(context: SubtitleContext): string | React.ReactNode {
+ if (!context.execution.createdAt) {
+ return "";
+ }
+ return renderTimeAgo(new Date(context.execution.createdAt));
+ },
+};
+
+function createBucketMetadataList(node: NodeInfo): MetadataItem[] {
+ const metadata: MetadataItem[] = [];
+ const configuration = node.configuration as CreateBucketConfiguration | undefined;
+
+ const bucketName = configuration?.bucketName?.trim();
+ if (bucketName) {
+ metadata.push({ icon: "database", label: bucketName });
+ }
+
+ return metadata;
+}
+
+function createBucketEventSections(nodes: NodeInfo[], execution: ExecutionInfo, componentName: string): EventSection[] {
+ const rootTriggerNode = nodes.find((n) => n.id === execution.rootEvent?.nodeId);
+ const rootTriggerRenderer = getTriggerRenderer(rootTriggerNode?.componentName ?? "");
+ const { title } = rootTriggerRenderer.getTitleAndSubtitle({ event: execution.rootEvent });
+
+ return [
+ {
+ receivedAt: new Date(execution.createdAt!),
+ eventTitle: title,
+ eventSubtitle: renderTimeAgo(new Date(execution.createdAt!)),
+ eventState: getState(componentName)(execution),
+ eventId: execution.rootEvent!.id!,
+ },
+ ];
+}
diff --git a/web_src/src/pages/app/mappers/aws/s3/delete_bucket.spec.ts b/web_src/src/pages/app/mappers/aws/s3/delete_bucket.spec.ts
new file mode 100644
index 0000000000..d8b4ae3150
--- /dev/null
+++ b/web_src/src/pages/app/mappers/aws/s3/delete_bucket.spec.ts
@@ -0,0 +1,105 @@
+import { describe, expect, it } from "vitest";
+
+import type {
+ ComponentBaseContext,
+ ComponentDefinition,
+ ExecutionDetailsContext,
+ ExecutionInfo,
+ NodeInfo,
+ OutputPayload,
+} from "../../types";
+import { deleteBucketMapper } from "./delete_bucket";
+import { eventStateRegistry } from "../index";
+
+function buildNode(overrides?: Partial): NodeInfo {
+ return {
+ id: "node-1",
+ name: "Delete Bucket Node",
+ componentName: "aws.s3.deleteBucket",
+ isCollapsed: false,
+ configuration: {},
+ metadata: {},
+ ...overrides,
+ };
+}
+
+function buildOutput(data: unknown): OutputPayload {
+ return { type: "aws.s3.bucket.deleted", timestamp: new Date().toISOString(), data };
+}
+
+function buildExecution(overrides?: Partial): ExecutionInfo {
+ return {
+ id: "exec-1",
+ createdAt: "2026-06-01T10:00:00.000Z",
+ updatedAt: "2026-06-01T10:00:05.000Z",
+ state: "STATE_FINISHED",
+ result: "RESULT_PASSED",
+ resultReason: "RESULT_REASON_OK",
+ resultMessage: "",
+ metadata: {},
+ configuration: {},
+ rootEvent: undefined,
+ ...overrides,
+ };
+}
+
+function buildDetailsCtx(overrides?: { node?: Partial; execution?: Partial }): ExecutionDetailsContext {
+ const node = buildNode(overrides?.node);
+ return { nodes: [node], node, execution: buildExecution(overrides?.execution) };
+}
+
+const defaultDefinition: ComponentDefinition = {
+ name: "aws.s3.deleteBucket",
+ label: "S3 • Delete Bucket",
+ description: "",
+ icon: "aws",
+ color: "gray",
+};
+
+function buildPropsContext(overrides?: Partial): ComponentBaseContext {
+ return {
+ nodes: [],
+ node: buildNode(),
+ componentDefinition: defaultDefinition,
+ lastExecutions: [],
+ currentUser: undefined,
+ actions: { invokeNodeExecutionHook: async () => {} },
+ ...overrides,
+ };
+}
+
+const deletedOutput = { bucketName: "my-example-bucket", deleted: true };
+
+describe("deleteBucketMapper.getExecutionDetails", () => {
+ it("does not throw when outputs is undefined", () => {
+ const ctx = buildDetailsCtx({ execution: { outputs: undefined } });
+ expect(() => deleteBucketMapper.getExecutionDetails(ctx)).not.toThrow();
+ });
+
+ it("extracts deletion fields from output", () => {
+ const ctx = buildDetailsCtx({ execution: { outputs: { default: [buildOutput(deletedOutput)] } } });
+ const details = deleteBucketMapper.getExecutionDetails(ctx);
+ expect(details["Bucket"]).toBe("my-example-bucket");
+ expect(details["Deleted"]).toBe("Yes");
+ });
+});
+
+describe("deleteBucketMapper.props", () => {
+ it("uses node name as title", () => {
+ expect(deleteBucketMapper.props(buildPropsContext()).title).toBe("Delete Bucket Node");
+ });
+
+ it("includes bucket from configuration in metadata", () => {
+ const props = deleteBucketMapper.props(
+ buildPropsContext({ node: buildNode({ configuration: { bucket: "my-example-bucket", region: "us-east-1" } }) }),
+ );
+ const labels = props.metadata?.map((m) => m.label) ?? [];
+ expect(labels).toContain("my-example-bucket");
+ });
+});
+
+describe("eventStateRegistry['s3.deleteBucket']", () => {
+ it("maps finished success to deleted", () => {
+ expect(eventStateRegistry["s3.deleteBucket"].getState(buildExecution())).toBe("deleted");
+ });
+});
diff --git a/web_src/src/pages/app/mappers/aws/s3/delete_bucket.ts b/web_src/src/pages/app/mappers/aws/s3/delete_bucket.ts
new file mode 100644
index 0000000000..23ea9ef80d
--- /dev/null
+++ b/web_src/src/pages/app/mappers/aws/s3/delete_bucket.ts
@@ -0,0 +1,99 @@
+import type {
+ ComponentBaseContext,
+ ComponentBaseMapper,
+ ExecutionDetailsContext,
+ ExecutionInfo,
+ NodeInfo,
+ OutputPayload,
+ SubtitleContext,
+} from "../../types";
+import type { ComponentBaseProps, EventSection } from "@/ui/componentBase";
+import type React from "react";
+import { getBackgroundColorClass, getColorClass } from "@/lib/colors";
+import { getState, getStateMap, getTriggerRenderer } from "../..";
+import awsIcon from "@/assets/icons/integrations/aws.svg";
+import { renderTimeAgo } from "@/components/TimeAgo";
+import type { MetadataItem } from "@/ui/metadataList";
+import { stringOrDash } from "../../utils";
+
+interface DeleteBucketConfiguration {
+ region?: string;
+ bucket?: string;
+}
+
+interface DeleteBucketOutput {
+ bucketName?: string;
+ deleted?: boolean;
+}
+
+export const deleteBucketMapper: ComponentBaseMapper = {
+ props(context: ComponentBaseContext): ComponentBaseProps {
+ const lastExecution = context.lastExecutions.length > 0 ? context.lastExecutions[0] : null;
+ const componentName = context.componentDefinition.name || "unknown";
+
+ return {
+ title:
+ context.node.name ||
+ context.componentDefinition.label ||
+ context.componentDefinition.name ||
+ "Unnamed component",
+ iconSrc: awsIcon,
+ iconColor: getColorClass(context.componentDefinition.color),
+ collapsedBackground: getBackgroundColorClass(context.componentDefinition.color),
+ collapsed: context.node.isCollapsed,
+ eventSections: lastExecution ? deleteBucketEventSections(context.nodes, lastExecution, componentName) : undefined,
+ includeEmptyState: !lastExecution,
+ metadata: deleteBucketMetadataList(context.node),
+ eventStateMap: getStateMap(componentName),
+ };
+ },
+
+ getExecutionDetails(context: ExecutionDetailsContext): Record {
+ const outputs = context.execution.outputs as { default?: OutputPayload[] } | undefined;
+ const result = outputs?.default?.[0]?.data as DeleteBucketOutput | undefined;
+
+ if (!result) {
+ return {};
+ }
+
+ return {
+ Bucket: stringOrDash(result.bucketName),
+ Deleted: result.deleted ? "Yes" : "No",
+ };
+ },
+
+ subtitle(context: SubtitleContext): string | React.ReactNode {
+ if (!context.execution.createdAt) {
+ return "";
+ }
+ return renderTimeAgo(new Date(context.execution.createdAt));
+ },
+};
+
+function deleteBucketMetadataList(node: NodeInfo): MetadataItem[] {
+ const metadata: MetadataItem[] = [];
+ const configuration = node.configuration as DeleteBucketConfiguration | undefined;
+
+ const bucket = configuration?.bucket?.trim();
+ if (bucket) {
+ metadata.push({ icon: "database", label: bucket });
+ }
+
+ return metadata;
+}
+
+function deleteBucketEventSections(nodes: NodeInfo[], execution: ExecutionInfo, componentName: string): EventSection[] {
+ const rootTriggerNode = nodes.find((n) => n.id === execution.rootEvent?.nodeId);
+ const rootTriggerRenderer = getTriggerRenderer(rootTriggerNode?.componentName ?? "");
+ const { title } = rootTriggerRenderer.getTitleAndSubtitle({ event: execution.rootEvent });
+
+ return [
+ {
+ receivedAt: new Date(execution.createdAt!),
+ eventTitle: title,
+ eventSubtitle: renderTimeAgo(new Date(execution.createdAt!)),
+ eventState: getState(componentName)(execution),
+ eventId: execution.rootEvent!.id!,
+ },
+ ];
+}
diff --git a/web_src/src/pages/app/mappers/aws/s3/get_bucket.spec.ts b/web_src/src/pages/app/mappers/aws/s3/get_bucket.spec.ts
new file mode 100644
index 0000000000..5d30162890
--- /dev/null
+++ b/web_src/src/pages/app/mappers/aws/s3/get_bucket.spec.ts
@@ -0,0 +1,110 @@
+import { describe, expect, it } from "vitest";
+
+import type {
+ ComponentBaseContext,
+ ComponentDefinition,
+ ExecutionDetailsContext,
+ ExecutionInfo,
+ NodeInfo,
+ OutputPayload,
+} from "../../types";
+import { getBucketMapper } from "./get_bucket";
+import { eventStateRegistry } from "../index";
+
+function buildNode(overrides?: Partial): NodeInfo {
+ return {
+ id: "node-1",
+ name: "Get Bucket Node",
+ componentName: "aws.s3.getBucket",
+ isCollapsed: false,
+ configuration: {},
+ metadata: {},
+ ...overrides,
+ };
+}
+
+function buildOutput(data: unknown): OutputPayload {
+ return { type: "aws.s3.bucket", timestamp: new Date().toISOString(), data };
+}
+
+function buildExecution(overrides?: Partial): ExecutionInfo {
+ return {
+ id: "exec-1",
+ createdAt: "2026-06-01T10:00:00.000Z",
+ updatedAt: "2026-06-01T10:00:05.000Z",
+ state: "STATE_FINISHED",
+ result: "RESULT_PASSED",
+ resultReason: "RESULT_REASON_OK",
+ resultMessage: "",
+ metadata: {},
+ configuration: {},
+ rootEvent: undefined,
+ ...overrides,
+ };
+}
+
+function buildDetailsCtx(overrides?: { node?: Partial; execution?: Partial }): ExecutionDetailsContext {
+ const node = buildNode(overrides?.node);
+ return { nodes: [node], node, execution: buildExecution(overrides?.execution) };
+}
+
+const defaultDefinition: ComponentDefinition = {
+ name: "aws.s3.getBucket",
+ label: "S3 • Get Bucket",
+ description: "",
+ icon: "aws",
+ color: "gray",
+};
+
+function buildPropsContext(overrides?: Partial): ComponentBaseContext {
+ return {
+ nodes: [],
+ node: buildNode(),
+ componentDefinition: defaultDefinition,
+ lastExecutions: [],
+ currentUser: undefined,
+ actions: { invokeNodeExecutionHook: async () => {} },
+ ...overrides,
+ };
+}
+
+const bucketOutput = {
+ bucketName: "my-example-bucket",
+ region: "eu-west-1",
+ arn: "arn:aws:s3:::my-example-bucket",
+};
+
+describe("getBucketMapper.getExecutionDetails", () => {
+ it("does not throw when outputs is undefined", () => {
+ const ctx = buildDetailsCtx({ execution: { outputs: undefined } });
+ expect(() => getBucketMapper.getExecutionDetails(ctx)).not.toThrow();
+ });
+
+ it("extracts bucket fields from output", () => {
+ const ctx = buildDetailsCtx({ execution: { outputs: { default: [buildOutput(bucketOutput)] } } });
+ const details = getBucketMapper.getExecutionDetails(ctx);
+ expect(details["Bucket"]).toBe("my-example-bucket");
+ expect(details["Region"]).toBe("eu-west-1");
+ expect(details["ARN"]).toBe("arn:aws:s3:::my-example-bucket");
+ });
+});
+
+describe("getBucketMapper.props", () => {
+ it("uses node name as title", () => {
+ expect(getBucketMapper.props(buildPropsContext()).title).toBe("Get Bucket Node");
+ });
+
+ it("includes bucket from configuration in metadata", () => {
+ const props = getBucketMapper.props(
+ buildPropsContext({ node: buildNode({ configuration: { bucket: "my-example-bucket", region: "eu-west-1" } }) }),
+ );
+ const labels = props.metadata?.map((m) => m.label) ?? [];
+ expect(labels).toContain("my-example-bucket");
+ });
+});
+
+describe("eventStateRegistry['s3.getBucket']", () => {
+ it("maps finished success to retrieved", () => {
+ expect(eventStateRegistry["s3.getBucket"].getState(buildExecution())).toBe("retrieved");
+ });
+});
diff --git a/web_src/src/pages/app/mappers/aws/s3/get_bucket.ts b/web_src/src/pages/app/mappers/aws/s3/get_bucket.ts
new file mode 100644
index 0000000000..a17e2cb263
--- /dev/null
+++ b/web_src/src/pages/app/mappers/aws/s3/get_bucket.ts
@@ -0,0 +1,101 @@
+import type {
+ ComponentBaseContext,
+ ComponentBaseMapper,
+ ExecutionDetailsContext,
+ ExecutionInfo,
+ NodeInfo,
+ OutputPayload,
+ SubtitleContext,
+} from "../../types";
+import type { ComponentBaseProps, EventSection } from "@/ui/componentBase";
+import type React from "react";
+import { getBackgroundColorClass, getColorClass } from "@/lib/colors";
+import { getState, getStateMap, getTriggerRenderer } from "../..";
+import awsIcon from "@/assets/icons/integrations/aws.svg";
+import { renderTimeAgo } from "@/components/TimeAgo";
+import type { MetadataItem } from "@/ui/metadataList";
+import { stringOrDash } from "../../utils";
+
+interface GetBucketConfiguration {
+ region?: string;
+ bucket?: string;
+}
+
+interface GetBucketOutput {
+ bucketName?: string;
+ region?: string;
+ arn?: string;
+}
+
+export const getBucketMapper: ComponentBaseMapper = {
+ props(context: ComponentBaseContext): ComponentBaseProps {
+ const lastExecution = context.lastExecutions.length > 0 ? context.lastExecutions[0] : null;
+ const componentName = context.componentDefinition.name || "unknown";
+
+ return {
+ title:
+ context.node.name ||
+ context.componentDefinition.label ||
+ context.componentDefinition.name ||
+ "Unnamed component",
+ iconSrc: awsIcon,
+ iconColor: getColorClass(context.componentDefinition.color),
+ collapsedBackground: getBackgroundColorClass(context.componentDefinition.color),
+ collapsed: context.node.isCollapsed,
+ eventSections: lastExecution ? getBucketEventSections(context.nodes, lastExecution, componentName) : undefined,
+ includeEmptyState: !lastExecution,
+ metadata: getBucketMetadataList(context.node),
+ eventStateMap: getStateMap(componentName),
+ };
+ },
+
+ getExecutionDetails(context: ExecutionDetailsContext): Record {
+ const outputs = context.execution.outputs as { default?: OutputPayload[] } | undefined;
+ const result = outputs?.default?.[0]?.data as GetBucketOutput | undefined;
+
+ if (!result) {
+ return {};
+ }
+
+ return {
+ Bucket: stringOrDash(result.bucketName),
+ Region: stringOrDash(result.region),
+ ARN: stringOrDash(result.arn),
+ };
+ },
+
+ subtitle(context: SubtitleContext): string | React.ReactNode {
+ if (!context.execution.createdAt) {
+ return "";
+ }
+ return renderTimeAgo(new Date(context.execution.createdAt));
+ },
+};
+
+function getBucketMetadataList(node: NodeInfo): MetadataItem[] {
+ const metadata: MetadataItem[] = [];
+ const configuration = node.configuration as GetBucketConfiguration | undefined;
+
+ const bucket = configuration?.bucket?.trim();
+ if (bucket) {
+ metadata.push({ icon: "database", label: bucket });
+ }
+
+ return metadata;
+}
+
+function getBucketEventSections(nodes: NodeInfo[], execution: ExecutionInfo, componentName: string): EventSection[] {
+ const rootTriggerNode = nodes.find((n) => n.id === execution.rootEvent?.nodeId);
+ const rootTriggerRenderer = getTriggerRenderer(rootTriggerNode?.componentName ?? "");
+ const { title } = rootTriggerRenderer.getTitleAndSubtitle({ event: execution.rootEvent });
+
+ return [
+ {
+ receivedAt: new Date(execution.createdAt!),
+ eventTitle: title,
+ eventSubtitle: renderTimeAgo(new Date(execution.createdAt!)),
+ eventState: getState(componentName)(execution),
+ eventId: execution.rootEvent!.id!,
+ },
+ ];
+}
diff --git a/web_src/src/pages/app/mappers/aws/s3/index.ts b/web_src/src/pages/app/mappers/aws/s3/index.ts
new file mode 100644
index 0000000000..88b65dfb1d
--- /dev/null
+++ b/web_src/src/pages/app/mappers/aws/s3/index.ts
@@ -0,0 +1,3 @@
+export { createBucketMapper } from "./create_bucket";
+export { getBucketMapper } from "./get_bucket";
+export { deleteBucketMapper } from "./delete_bucket";