Skip to main content

REUSE A BEHAVIOR WITH THE FACTORY PATTERN

This guide shows you how to define a single behavior and call it multiple times with different inputs to produce distinct resource instances. Use this when you have several resources of the same type — subnets, security groups, DNS records — that share the same schema but differ in their parameters.

Prerequisites

Decide whether the factory pattern fits

The factory pattern applies when multiple resources share a type and method signatures but differ only in their parameter values.

Situation Approach
4 subnets with different CIDRs and AZs Factory pattern (1 behavior)
3 EIPs with different tags Factory pattern (1 behavior)
A VPC and a subnet (different resource types) Separate behaviors
Resources with different method signatures Separate behaviors
Long-running methods that must run in parallel Factory pattern (separate locks)

Each behavior holds an exclusive per-behavior lock for the duration of a method execution. If two workflow steps target the same behavior, the second waits for the first to release the lock. The factory pattern sidesteps this: each call with a different instanceName produces a separate data instance, so concurrent steps do not contend.

Add an inputs schema to the behavior

Create the behavior, then add an inputs section with an instanceName property and whatever parameters vary between instances:

swamp behavior create @user/aws-subnet prod-subnet --json

Edit the behavior definition at the returned path:

name: prod-subnet
version: 1
tags: {}
inputs:
  properties:
    instanceName:
      type: string
      description: Unique name for this subnet instance
    cidrBlock:
      type: string
      description: CIDR block for the subnet
    availabilityZone:
      type: string
      description: AWS availability zone
  required: ["instanceName", "cidrBlock", "availabilityZone"]
globalArguments:
  name: ${{ inputs.instanceName }}
  VpcId: ${{ data.latest("prod-vpc", "main").attributes.VpcId }}
  CidrBlock: ${{ inputs.cidrBlock }}
  AvailabilityZone: ${{ inputs.availabilityZone }}
  Tags:
    - Key: Name
      Value: ${{ inputs.instanceName }}
methods:
  create:
    arguments: {}
  delete:
    arguments: {}

The name: ${{ inputs.instanceName }} line is what makes the pattern work. It sets the data instance name, so each call with a different instanceName produces a separately addressable data artifact.

Validate the behavior:

swamp behavior validate prod-subnet --json

Call the behavior multiple times from a workflow

Create a workflow with one step per instance. Steps within a job run in parallel:

name: create-subnets
version: 1
jobs:
  - name: create-subnets
    steps:
      - name: create-public-a
        task:
          type: behavior
          behavior: prod-subnet
          methodName: create
          inputs:
            instanceName: public-a
            cidrBlock: "10.0.1.0/24"
            availabilityZone: us-east-1a
      - name: create-public-b
        task:
          type: behavior
          behavior: prod-subnet
          methodName: create
          inputs:
            instanceName: public-b
            cidrBlock: "10.0.2.0/24"
            availabilityZone: us-east-1b
      - name: create-private-a
        task:
          type: behavior
          behavior: prod-subnet
          methodName: create
          inputs:
            instanceName: private-a
            cidrBlock: "10.0.3.0/24"
            availabilityZone: us-east-1a
      - name: create-private-b
        task:
          type: behavior
          behavior: prod-subnet
          methodName: create
          inputs:
            instanceName: private-b
            cidrBlock: "10.0.4.0/24"
            availabilityZone: us-east-1b

Run the workflow:

swamp workflow run create-subnets

All four steps execute concurrently. Each produces a distinct data instance (public-a, public-b, private-a, private-b) under the prod-subnet behavior.

Reference factory instance data downstream

Use data.latest() with the behavior name and instance name to read a specific factory output in a downstream behavior or workflow step:

globalArguments:
  subnetId: ${{ data.latest("prod-subnet", "public-a").attributes.SubnetId }}

To browse all instances produced by a factory behavior:

swamp data query 'behaviorName == "prod-subnet"' \
  --select '{"name": name, "subnet": attributes.SubnetId, "az": attributes.AvailabilityZone}'

Write delete steps for factory behaviors

Delete steps must provide instanceName so the system knows which data instance to target. Other inputs are only required if the delete method's implementation accesses those globalArguments at runtime — unresolved expressions for inputs that are not provided are silently skipped.

name: delete-subnets
version: 1
jobs:
  - name: delete-subnets
    steps:
      - name: delete-public-a
        task:
          type: behavior
          behavior: prod-subnet
          methodName: delete
          inputs:
            instanceName: public-a
            identifier: ${{ data.latest("prod-subnet", "public-a").attributes.SubnetId }}
      - name: delete-public-b
        task:
          type: behavior
          behavior: prod-subnet
          methodName: delete
          inputs:
            instanceName: public-b
            identifier: ${{ data.latest("prod-subnet", "public-b").attributes.SubnetId }}

If the delete steps depend on other resources being removed first, use job-level dependsOn to enforce reverse dependency order — see dependsOn.

Reference

  • Behavior definitions — full schema for inputs, globalArguments, and methods
  • CEL expressions — data.latest(), data.query(), and the inputs.* namespace
  • Workflows — job and step ordering, dependsOn, and parallel execution