MAKE WORKFLOWS IDEMPOTENT
This guide shows you how to add guard expressions to workflow steps so that re-running a workflow skips steps whose work is already done.
Prerequisites
- Swamp installed and on your
PATH - An initialised repo (
swamp repo init) - A workflow with at least one step that produces data
- Familiarity with workflow definitions and CEL expressions
Guard a step with data truthiness
Add a guard field to any step. The value is a CEL expression wrapped in
${{ }}. When the expression evaluates to a truthy value, the step is skipped.
If your step produces data via a model method, check whether that data already exists:
steps:
- name: provision
guard: "${{ data.latest('provisioner', 'result') }}"
task:
type: model_method
modelIdOrName: provisioner
methodName: execute
inputs:
run: "echo 'provisioned'"On the first run, data.latest('provisioner', 'result') returns a falsy value
(no data exists yet), so the step executes:
system │ Starting workflow provision-stack
main │ start
main │ step provision · provisioner · execute · start
main │ provisioned
main │ done provision
system │ Completed workflow provision-stack succeededOn the second run, the data exists and the guard evaluates truthy. The step is skipped:
main │ skipped (guarded)
system │ Starting workflow provision-stack
main │ start
main │ completed
system │ Completed workflow provision-stack succeededGuard with a value comparison
When data existence alone is not sufficient, compare a specific attribute against an expected value:
steps:
- name: configure-vpc
guard: "${{ data.latest('vpc', 'result').attributes.cidr == '10.0.0.0/16' }}"
task:
type: model_method
modelIdOrName: vpc
methodName: create
inputs:
cidr: "10.0.0.0/16"The step re-runs if the VPC's CIDR has drifted from the expected value.
Guard with a method call
If you need to check live state rather than stored data, call a model method inside the guard expression:
steps:
- name: provision-dns
guard: "${{ model.method('dns-checker', 'verify', {'domain': 'app.example.com'}).stdout }}"
task:
type: model_method
modelIdOrName: dns-provisioner
methodName: create
inputs:
domain: app.example.comGuard forEach iterations independently
When a step uses forEach, the guard evaluates once per iteration. Each
iteration can be skipped or executed independently based on its own guard
result. Use self.{item} to vary the guard per iteration:
steps:
- name: provision-${{ self.region }}
guard: "${{ data.latest('provisioner-' + self.region, 'result') }}"
forEach:
item: region
in: "${{ inputs.regions }}"
task:
type: model_method
modelType: command/shell
modelName: provisioner-${{ self.region }}
methodName: execute
inputs:
run: "echo 'provisioned ${{ self.region }}'"On the first run, both iterations execute:
main │ start
main │ step provision-us-east-1 · provisioner-us-east-1 · execute · start
main │ step provision-eu-west-1 · provisioner-eu-west-1 · execute · start
main │ provisioned eu-west-1
main │ provisioned us-east-1
main │ done provision-eu-west-1
main │ done provision-us-east-1
main │ completed
system │ Completed workflow provision-stack succeededOn the second run, both iterations are skipped:
main │ skipped (guarded)
main │ skipped (guarded)
system │ Starting workflow provision-stack
main │ start
main │ completed
system │ Completed workflow provision-stack succeededIf you add a new region to the input list, only the new region's iteration executes — the existing regions remain guarded.
Inspect guarded skips in JSON output
Run with --json to see structured skip events:
swamp workflow run provision-stack --jsonEach guarded step emits a line:
{ "step": "provision", "job": "main", "status": "skipped", "reason": "guarded" }The reason field distinguishes guarded skips ("guarded") from dependency
skips ("dependency").
Reference
guardfield reference — field type, evaluation semantics, and error handling- CEL Expressions — full expression language and context variables
- The Workflow Execution Model — how steps, dependencies, and parallelism work