WORKFLOWS
A workflow is a YAML file that orchestrates the execution of model methods and
other workflows. Workflows live in the workflows/ directory within a Swamp
repository.
File Structure
workflows/
workflow-e182b5c7-41e7-4c9a-a9f8-455b7bb5ce12.yaml
workflow-d14de37a-b931-4363-97e9-fe9ec9637b60.yamlThe filename is workflow-{uuid}.yaml.
Top-Level Fields
id: d14de37a-b931-4363-97e9-fe9ec9637b60
name: deploy-pipeline
description: A multi-stage deployment pipeline
trigger:
schedule: "0 3 * * *"
tags:
team: platform
inputs:
type: object
properties:
region:
type: string
default: us-east-1
required:
- region
jobs:
- name: build
steps:
- name: compile
task:
type: model_method
modelIdOrName: builder
methodName: run
version: 1
reports:
require:
- summary
skip:
- debug-reportid
Unique identifier for the workflow.
| Property | Value |
|---|---|
| Type | string (UUID v4) |
| Required | Yes |
| Default | Auto-generated on create |
| Format | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
name
Human-readable name for the workflow.
| Property | Value |
|---|---|
| Type | string |
| Required | Yes |
| Default | None |
Constraints:
- Minimum 1 character.
- Must be unique within the repository.
- Must not contain
..,\, or null bytes (path traversal protection). /is only allowed in scoped names matching@[a-z0-9_-]+/[a-z0-9_-]+(\/[a-z0-9_-]+)*(e.g.,@team/deploy-pipeline).
description
Human-readable description of the workflow's purpose.
| Property | Value |
|---|---|
| Type | string |
| Required | No |
| Default | None |
trigger
Scheduling configuration for automatic execution.
| Property | Value |
|---|---|
| Type | object |
| Required | No |
| Default | None |
trigger.schedule
Cron expression for recurring execution.
| Property | Value |
|---|---|
| Type | string |
| Required | No |
| Format | Standard cron (5 fields): minute hour dom month dow |
Validated at parse time using the Croner library. Invalid expressions cause a schema validation error.
trigger:
schedule: "0 3 * * *" # Daily at 3:00 AMtrigger:
schedule: "*/15 * * * *" # Every 15 minutestrigger:
schedule: "0 3,12 * * *" # 3:00 AM and 12:00 PM dailytrigger.inputs
Baseline input values supplied to trigger-fired runs. Trigger-fired runs
(scheduled and webhook) have no --input flag, so trigger.inputs provides the
values at fire time.
| Property | Value |
|---|---|
| Type | Record<string, unknown> |
| Required | No |
| Default | None |
This is a free values map — the runtime values to inject — distinct from the
workflow's inputs block, which is the JSON Schema describing
allowed inputs.
trigger:
schedule: "0 3 * * *"
inputs:
projectId: "a6b254a2-0b57-4d0f-bf8b-fef767ab119e"Values merge as caller inputs > trigger.inputs > schema defaults — the same
merge as --input on swamp workflow run. A scheduled run has no caller, so
trigger.inputs is the baseline. A webhook run extracts no inputs from its
payload (the body is used only for signature verification), so it also takes its
baseline from trigger.inputs. The merged values pass through the same
coercion, default application, and validation as every other run.
trigger.inputs lets a workflow declare required inputs rather than relying
on the schema default, which would apply to every caller. It applies only to
trigger-fired runs; a manual swamp workflow run supplies inputs explicitly and
is unaffected. See Webhook Triggers for webhook-fired runs.
tags
Key-value labels attached to the workflow.
| Property | Value |
|---|---|
| Type | Record<string, string> |
| Required | No |
| Default | {} |
tags:
team: platform
env: productioninputs
JSON Schema definition for structured inputs provided at runtime. Uses the same
InputsSchema format as
model definition inputs.
| Property | Value |
|---|---|
| Type | InputsSchema |
| Required | No |
| Default | None |
inputs:
type: object
properties:
environments:
type: array
description: Target environments
region:
type: string
default: us-east-1
required:
- environmentsInput values are available in CEL expressions as inputs.* within step task
inputs and forEach expressions.
Input values are inert and recorded
Input values are not evaluated as expressions. A ${{ ... }} in a value passed
with swamp workflow run --input, --input-file, or --stdin stays literal
text, and Swamp logs a warning naming the path. See
Where Expressions Are Evaluated.
Input values, after type coercion and schema defaults, are persisted in:
- the run record and the run history index (
swamp workflow history searchfilters on them) - the run record of any child workflow they are passed to
- the evaluated model definition of any step that substitutes them
- the data of any model that records its arguments (for example, the
commandfield of acommand/shellresult)
The same applies to values supplied at resume.
Input schemas have no sensitive property. A sensitive: true key on an input
is accepted and has no effect.
Never pass a secret as an input value. Pass the vault name and key, and resolve
the secret in the step with
vault.get. Vault values are
resolved per step, redacted in step data, and left unresolved in evaluated
definitions:
# Wrong: the shell expands the variable and the secret is recorded
swamp workflow run deploy --input apiKey="<value of an env var>"
# Right: only the vault reference is recorded
swamp workflow run deploy --input vaultName=prod-secrets --input secretKey=API_KEYinputs:
properties:
vaultName: { type: string }
secretKey: { type: string }
jobs:
- name: deploy
steps:
- name: call-api
task:
type: model_method
modelIdOrName: api-client
methodName: invoke
inputs:
apiKey: ${{ vault.get(inputs.vaultName, inputs.secretKey) }}${{ env.* }} values are substituted as plain text and are not redacted in step
data, so an environment variable holding the secret is not a substitute for a
vault. An environment variable may supply the vault or key name, as in
vault.get(prod-secrets, env.API_KEY_NAME).
jobs
Ordered list of jobs to execute.
| Property | Value |
|---|---|
| Type | Job[] |
| Required | Yes |
| Minimum | 1 |
See Job below.
version
Workflow version number.
| Property | Value |
|---|---|
| Type | integer |
| Required | No |
| Default | 1 |
Must be a positive integer.
concurrency
Maximum number of jobs that execute in parallel at each dependency level.
| Property | Value |
|---|---|
| Type | integer |
| Required | No |
| Default | None |
Must be a non-negative integer. When unset or 0, parallelism is unbounded.
Jobs and steps can override this value. See
Concurrency Resolution Order.
concurrency: 4target
Default target for all steps. Inherited by jobs and steps unless overridden.
See Workflow Placement
for the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | string |
| Required | No |
| Default | None |
Mutually exclusive with labels.
labels
Default labels for all steps. Inherited by jobs and steps unless overridden.
See Workflow Placement
for the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | Record<string, string> |
| Required | No |
| Default | None |
Mutually exclusive with target.
platform
Default platform for all steps. Inherited by jobs and steps unless overridden.
See Workflow Placement
for the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | string |
| Required | No |
| Default | None |
queueTimeout
Default queueTimeout for all steps. Inherited by jobs and steps unless
overridden. See
Workflow Placement for
the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | number (seconds) |
| Required | No |
| Default | None |
affinity
When true, all remote steps in the workflow run on the same worker node. The
dispatch service picks a worker for the first placed step via normal
target/labels/platform matching, then pins all subsequent placed steps to
that same worker for the duration of the workflow run. Inherited by jobs unless
overridden. See
Workflow Placement — Affinity
for behavior details, failure semantics, and the concurrency trade-off.
| Property | Value |
|---|---|
| Type | boolean |
| Required | No |
| Default | false |
Affinity requires placement (target, labels, or platform) to have any
effect — without placement, steps run locally and affinity is a no-op (a
validation warning is emitted). Mutually exclusive with step-level target
overrides within the affinity scope: if a step sets its own target, it opts
out of the affinity group.
name: gpu-pipeline
labels:
gpu: "true"
affinity: true
jobs:
- name: train
steps:
- name: prepare-data
task:
type: model_method
modelIdOrName: preprocessor
methodName: run
- name: fit-model
task:
type: model_method
modelIdOrName: trainer
methodName: run
dependsOn:
- step: prepare-data
condition:
type: succeededBoth prepare-data and fit-model run on the same GPU worker — the first step
selects the worker, and the second is pinned to it.
autoResume
Whether swamp serve resumes a suspended run itself once an approval made
through serve decides the run's last manual_approval gate. See
Auto-resume.
| Property | Value |
|---|---|
| Type | boolean |
| Required | No |
| Default | None (the serve --auto-resume setting) |
autoResume: true or autoResume: false always overrides the serve
--auto-resume setting. When autoResume is unset, the serve setting applies
only if the workflow declares no inputs.
reports
Post-execution report selection.
| Property | Value |
|---|---|
| Type | ReportSelection |
| Required | No |
| Default | None |
Uses the same format as model definition reports.
A workflow-scope report receives the workflow run as its context — see
Workflow-scope context.
Downstream steps read earlier steps through the
steps namespace, which carries only
status and outputs.
reports:
require:
- summary
- name: deployment-report
methods:
- apply
skip:
- debug-reportRequired reports must resolve or the workflow run fails. The resolution rules are the same as for model definition reports — an unresolvable name logs a warning, writes an error artifact, and counts as a report failure.
swamp workflow resume executes workflow-scope and step-scope reports,
including reports.require. Previously, resumed workflows skipped all reports.
vaults
The most vaults any run of the workflow may read or write.
| Property | Value |
|---|---|
| Type | string[] |
| Required | No |
| Default | None (no limit from the file) |
name: provision-room
vaults: [roomcontrol, bot-outputs]Every vault operation in a run of the workflow — vault.get expressions, method
code using context.vaultService, sensitive outputs and their read-back — on a
vault not listed is refused, whoever triggered the run:
Failed to resolve vault expression vault.get("erp", "password"): Reading vault 'erp' is refused: it is not in the vaults list of workflow 'provision-room'.- The list applies to local
swamp workflow runas well as serve runs. On a serve run it is checked alongside the triggering principal's vault grants, and either can refuse. - A nested workflow's list intersects with its parent's, so nesting only narrows.
- A run records the list in force when it starts. A resume is held to both the recorded list and the workflow's current list, so editing the workflow can narrow a suspended run but never widen it.
- The list must include the vault each step's sensitive outputs land in. A mutating method whose sensitive outputs would land in an unlisted vault is refused before it runs.
- A model method run outside a workflow has no list.
swamp workflow validate reports a static vault.get name in the workflow's
own steps or inputs that is not listed, and each sensitive-output vault of a
model-method step that is not listed:
✗ Vaults the workflow reads are in its vaults list
→ vault.get reads 'erp', not in the workflow's vaults list: add it to vaults or read a listed vaultDynamic vault names and vault.get inside model definitions are caught only at
run time.
A release older than the field rejects a workflow file that sets vaults:.
Upgrade every machine and serve replica that loads the workflow first.
Webhook Triggers
A workflow can be triggered by an external HTTP webhook in addition to a
trigger.schedule cron. Webhook endpoints are registered on
the worker, not in the workflow file, with the --webhook flag of
swamp serve:
swamp serve --webhook /hooks/github:my-workflow:$WEBHOOK_SECRETThe flag value is <route>:<workflow>:<secret>. The route must start with /;
the secret is everything after the second colon, so it may itself contain
colons.
Request contract
A registered route accepts POST requests. Each request must carry an
X-Hub-Signature-256 header whose value is sha256=<hex>, where <hex> is the
HMAC-SHA256 of the raw request body keyed by the shared secret (the GitHub
webhook scheme).
| Status | Condition |
|---|---|
200 |
Valid signature — the run is queued: { "status": "queued" } |
401 |
Missing or invalid X-Hub-Signature-256 signature |
413 |
Request body exceeds 10 MB |
503 |
The run queue is full (maximum 100 queued runs) |
A queued run executes through the same path as scheduled and CLI runs.
Inputs
The request body is used only for signature verification — no inputs are
extracted from the payload. A webhook-fired run takes its baseline input values
from trigger.inputs, exactly like a scheduled run.
Job
A job groups related steps within a workflow. Jobs with no dependencies on each other execute in parallel.
jobs:
- name: build
description: Compile and test
steps:
- name: compile
task:
type: model_method
modelIdOrName: builder
methodName: run
dependsOn: []
weight: 0name
Job name, unique within the workflow.
| Property | Value |
|---|---|
| Type | string |
| Required | Yes |
| Minimum | 1 char |
Any printable character is allowed, including Unicode, and space is the only whitespace. Control characters, tab and newline are refused.
description
Human-readable description of the job.
| Property | Value |
|---|---|
| Type | string |
| Required | No |
| Default | None |
steps
Ordered list of steps to execute within the job.
| Property | Value |
|---|---|
| Type | Step[] |
| Required | Yes |
| Minimum | 1 |
See Step below.
dependsOn
Dependencies on other jobs with trigger conditions.
| Property | Value |
|---|---|
| Type | JobDependency[] |
| Required | No |
| Default | [] |
Each element specifies which job to depend on and under what condition:
dependsOn:
- job: build
condition:
type: succeededSee Trigger Conditions for the full condition schema.
weight
Numeric weight for deterministic ordering of jobs at the same dependency level.
| Property | Value |
|---|---|
| Type | number |
| Required | No |
| Default | 0 |
Lower weights execute first. Jobs with the same weight and dependency level are ordered alphabetically by name.
concurrency
Maximum number of steps that execute in parallel at each dependency level within
this job. Overrides the workflow-level concurrency.
| Property | Value |
|---|---|
| Type | integer |
| Required | No |
| Default | None |
Must be a non-negative integer. When unset or 0, falls back to the
workflow-level value. See
Concurrency Resolution Order.
target
Default target for all steps in this job. Overrides the workflow-level
target. See
Workflow Placement for
the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | string |
| Required | No |
| Default | None |
Mutually exclusive with labels.
labels
Default labels for all steps in this job. Overrides the workflow-level
labels. See
Workflow Placement for
the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | Record<string, string> |
| Required | No |
| Default | None |
Mutually exclusive with target. An explicit labels: {} clears inherited
labels — steps in this job run locally unless they set their own placement.
platform
Default platform for all steps in this job. Overrides the workflow-level
platform. See
Workflow Placement for
the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | string |
| Required | No |
| Default | None |
queueTimeout
Default queueTimeout for all steps in this job. Overrides the workflow-level
queueTimeout. See
Workflow Placement for
the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | number (seconds) |
| Required | No |
| Default | None |
affinity
When true, all remote steps in this job run on the same worker node. Overrides
the workflow-level affinity. See
Workflow Placement — Affinity
for behavior details and failure semantics.
| Property | Value |
|---|---|
| Type | boolean |
| Required | No |
| Default | None |
When unset, falls back to the workflow-level affinity. Set to false
explicitly to opt a job out of a workflow-wide affinity group.
Step
A step is a single unit of work within a job. Steps with no dependencies on each other execute in parallel.
steps:
- name: deploy-env
description: Deploy to environment
task:
type: model_method
modelIdOrName: deployer
methodName: apply
forEach:
item: env
in: "${{ inputs.environments }}"
dependsOn:
- step: compile
condition:
type: succeeded
weight: 0
dataOutputOverrides:
- specName: result
lifetime: 30d
garbageCollection: 5
tags:
stage: deploy
vary:
- environment
allowFailure: falsename
Step name, unique within the job.
| Property | Value |
|---|---|
| Type | string |
| Required | Yes |
| Minimum | 1 char |
Any printable character is allowed, including Unicode, and space is the only whitespace. Control characters, tab and newline are refused.
When forEach is used and the step name contains ${{ }} expressions, the name
is evaluated with the iteration context to produce unique names per iteration.
The character rule applies to the name as written, so the spaces inside the
${{ }} delimiters are fine.
description
Human-readable description of the step.
| Property | Value |
|---|---|
| Type | string |
| Required | No |
| Default | None |
task
The work to execute. See Step Task below.
| Property | Value |
|---|---|
| Type | StepTask |
| Required | Yes |
forEach
Iteration configuration. When set, the step expands into one execution per element.
| Property | Value |
|---|---|
| Type | ForEach |
| Required | No |
| Default | None |
See ForEach below.
dependsOn
Dependencies on other steps within the same job.
| Property | Value |
|---|---|
| Type | StepDependency[] |
| Required | No |
| Default | [] |
dependsOn:
- step: compile
condition:
type: succeededSee Trigger Conditions for the full condition schema.
weight
Numeric weight for deterministic ordering of steps at the same dependency level.
| Property | Value |
|---|---|
| Type | number |
| Required | No |
| Default | 0 |
Lower weights execute first. Steps with the same weight and dependency level are ordered alphabetically by name.
dataOutputOverrides
Overrides for data output specifications produced by the step's task.
| Property | Value |
|---|---|
| Type | DataOutputOverride[] |
| Required | No |
| Default | None |
See DataOutputOverride below.
allowFailure
When true, a step failure does not fail the job. Subsequent steps with
succeeded conditions against this step will not trigger, but completed and
always conditions will.
| Property | Value |
|---|---|
| Type | boolean |
| Required | No |
| Default | false |
guard
A CEL expression evaluated before the step executes. When the expression evaluates to a truthy value, the step is skipped. When it evaluates to a falsy value (or is absent), the step executes normally.
| Property | Value |
|---|---|
| Type | string (CEL expression) |
| Required | No |
| Default | None |
The expression is wrapped in ${{ }} and has access to the same context as step
task.inputs: inputs, data, model, self, env, vault, file, run.
When forEach is active, the iteration variable (self.{item}) is available —
the guard evaluates once per iteration, so individual iterations can be skipped
independently.
guard: "${{ data.latest('deployer', 'result') }}"A CEL evaluation error in the guard expression fails the step.
A guarded step that is skipped emits a step_skipped event with
reason: "guarded", distinct from reason: "dependency" when a step is skipped
due to an unmet dependency condition. Console output names the skipped step and
shows the skip reason and the guard expression that triggered it:
skipped deploy-env (guarded) · guard: data.latest("checker", "result").attributes.exitCode == 0A forEach iteration's line names its expanded step, for example
fetch[0] │ skipped fetch-dev (guarded).
In --json output, the structured step_skipped line includes reason,
guardExpression, and guardResult fields:
{
"type": "step_skipped",
"step": "deploy-env",
"reason": "guarded",
"guardExpression": "data.latest(\"checker\", \"result\").attributes.exitCode == 0",
"guardResult": true
}At debug log level, guard evaluation is logged for every guarded step — both when the guard causes a skip and when it evaluates falsy and the step proceeds.
See the CEL Expressions reference for the full expression language. For goal-oriented usage, see Make Workflows Idempotent.
concurrency
Maximum number of steps that execute in parallel at this step's dependency level
within the job. Overrides job-level and workflow-level concurrency.
| Property | Value |
|---|---|
| Type | integer |
| Required | No |
| Default | None |
Must be a non-negative integer. When unset or 0, falls back to the job-level
value. See Concurrency Resolution Order.
target
Dispatch this step to a specific remote worker by name or instance UUID.
Overrides any target inherited from the job or workflow. See
Workflow Placement for
the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | string |
| Required | No |
| Default | None |
Mutually exclusive with labels.
labels
Dispatch this step to any connected remote worker whose labels are a superset of
this selector. Overrides any labels inherited from the job or workflow. An
explicit labels: {} clears inherited labels — the step runs locally unless it
has other placement fields. See
Workflow Placement for
the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | Record<string, string> |
| Required | No |
| Default | None |
Mutually exclusive with target.
platform
Dispatch this step to a worker running the specified operating system. Overrides
any platform inherited from the job or workflow. See
Workflow Placement for
the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | string |
| Required | No |
| Default | None |
queueTimeout
Override the server's default queue timeout for this step. Overrides any
queueTimeout inherited from the job or workflow. See
Workflow Placement for
the inheritance chain and scheduling behavior.
| Property | Value |
|---|---|
| Type | number (seconds) |
| Required | No |
| Default | None |
Step Task
A step task defines the work a step executes. Four task types are available.
Model Method (model_method)
Invokes a method on a model definition. Two mutually exclusive field sets identify the target model.
modelIdOrName variant
Targets an existing definition by name or UUID.
task:
type: model_method
modelIdOrName: deployer
methodName: apply
inputs:
target: "${{ inputs.region }}"| Field | Type | Required | Description |
|---|---|---|---|
type |
"model_method" |
Yes | Task type discriminator |
modelIdOrName |
string | Yes | Model definition name or UUID |
methodName |
string | Yes | Method to invoke |
inputs |
Record<string, unknown> | string |
No | Input values for the method (record or expression) |
globalArgs |
Record<string, unknown> | string |
No | Global arguments for the model definition |
modelType + modelName variant
Targets a model type directly. When no definition with the given name exists,
one is created automatically in .swamp/auto-definitions/. inputs values are
split between global arguments and method arguments using the type's schemas —
method arguments take precedence on ambiguous keys.
task:
type: model_method
modelType: "@swamp/aws/ec2/vpc"
modelName: my-vpc
methodName: create
inputs:
region: us-east-1
cidr: "10.0.0.0/16"| Field | Type | Required | Description |
|---|---|---|---|
type |
"model_method" |
Yes | Task type discriminator |
modelType |
string | Yes | Model type identifier |
modelName |
string | Yes | Name for the created definition |
methodName |
string | Yes | Method to invoke |
inputs |
Record<string, unknown> | string |
No | Input values for the method (record or expression) |
globalArgs |
Record<string, unknown> | string |
No | Global arguments for the model definition |
modelName supports CEL template expressions in forEach steps:
- name: scan-${{ self.host }}
forEach: { item: host, in: "${{ inputs.hosts }}" }
task:
type: model_method
modelType: "@swamp/cve/dirtyfrag"
modelName: fleet-scanner
methodName: scanFleet
inputs:
host: ${{ self.host }}When the target resolves
The task target — modelIdOrName, modelName, or a workflow task's
workflowIdOrName — resolves at run start, except in two cases where it
resolves when its step runs:
- The expression reads
data.*orsteps.*. - The step has a
guard.
A target that resolves when its step runs reads the records earlier steps of the
same run wrote. A step skipped by its guard never resolves its target.
methodName and modelType always resolve at run start.
- name: dispatch
dependsOn: [{ step: decide, condition: { type: succeeded } }]
task:
type: workflow
workflowIdOrName: ${{ data.latest("driver", "next").attributes.workflow }}Variant constraints
A step must contain exactly one of these field sets. Validation rejects:
- Both
modelIdOrNameandmodelTypepresent modelTypewithoutmodelName- Neither
modelIdOrNamenormodelTypepresent
inputs
inputs accepts two forms. The record form is a map of key-value pairs
where individual values may contain ${{ }} CEL expressions:
inputs:
target: "${{ inputs.region }}"
retries: 3The expression form is a single ${{ }} CEL expression that evaluates to a
record at runtime:
inputs: "${{ self.item.implementation.inputs }}"The expression must evaluate to a map — the runtime passes the resolved key-value pairs as input values. The two forms are mutually exclusive for a given task.
Both forms are supported on model_method and workflow task types.
globalArgs
globalArgs sets global arguments on the target model definition. It accepts
the same two forms as inputs — a literal record or a single
expression. Available on model_method tasks only.
task:
type: model_method
modelIdOrName: scanner
methodName: run
inputs:
target: "${{ self.host }}"
globalArgs:
region: "${{ inputs.region }}"When using the modelType + modelName variant without globalArgs, keys in
inputs are split between global arguments and method arguments using the
type's schemas. Setting globalArgs explicitly separates the two.
forEach with whole-field expressions
The expression form is particularly useful in forEach steps where each
iteration item carries its own input record. Instead of mapping individual keys,
pass the record through wholesale:
steps:
- name: run-${{ self.task.name }}
forEach:
item: task
in: "${{ inputs.tasks }}"
task:
type: model_method
modelType: "${{ self.task.modelType }}"
modelName: "${{ self.task.name }}"
methodName: run
inputs: "${{ self.task.inputs }}"
globalArgs: "${{ self.task.globalArgs }}"Given workflow inputs like:
tasks:
- name: scan-us
modelType: "@swamp/scanner"
inputs: { region: us-east-1, depth: full }
globalArgs: { pool: 4 }
- name: scan-eu
modelType: "@swamp/scanner"
inputs: { region: eu-west-1, depth: quick }
globalArgs: { pool: 2 }Each iteration expands self.task to the current element, resolving inputs
and globalArgs to the nested records without enumerating every key.
Workflow (workflow)
Invokes another workflow as a nested execution.
task:
type: workflow
workflowIdOrName: notification-workflow
inputs:
channel: "#deployments"| Field | Type | Required | Description |
|---|---|---|---|
type |
"workflow" |
Yes | Task type discriminator |
workflowIdOrName |
string | Yes | Workflow name or UUID; see when the target resolves |
inputs |
Record<string, unknown> | string |
No | Input values passed to the workflow (record or expression) |
Nested workflows have a maximum depth of 10. Cyclic references (workflow A invoking workflow B which invokes workflow A) are detected and rejected.
The child runs as a workflow run of its own, with its own run id, and appears separately in workflow history.
Manual approval in a nested workflow
When the child suspends at a manual_approval gate, the parent suspends too, on
the workflow step. The child is an ordinary suspended run: approve, reject,
resume, recover and cancel it under its own workflow name and run id.
| Child outcome | Parent, on resume |
|---|---|
succeeded |
The step adopts the child's outputs and the parent continues |
| Gate rejected | The step fails as a rejected approval |
| Not finished | The resume is refused, naming the commands that settle the child |
- Under
swamp serve, the parent resumes on its own once the child finishes, under the parent workflow's auto-resume policy, and only if the approver holds theapprovegrant on the parent. - Cancelling, rejecting or superseding the parent fails the waiting step as detached and leaves the child as it was. While the child is unfinished, the output prints the command that cancels it.
swamp workflow approvalslists the child's gate and names the parent run.- In
--jsonoutput, the suspended run'sapprovalRequirednames the gate to decide (workflowName,runId,stepId), andwaitingOnNestedRunnames the child (workflowName,runId).
See Approve a gate inside a nested workflow.
Manual Approval (manual_approval)
Suspends the workflow run and waits for an out-of-band approval decision before downstream steps execute.
task:
type: manual_approval
prompt: "Staging verified. Approve production rollout?"
timeout: 3600| Field | Type | Required | Description |
|---|---|---|---|
type |
"manual_approval" |
Yes | Task type discriminator |
prompt |
string | Yes | Message shown to the approver. Must be non-empty. |
timeout |
number | No | Approval window in seconds. Must be greater than 0. |
When timeout is set, an approval submitted after the window has elapsed is
rejected with an Approval timed out error and the gate remains unapproved. An
expired gate also no longer appears in swamp workflow approvals. When
timeout is omitted, the gate waits indefinitely.
Approving a gate does not continue the run; a resume does. Serve resumes the run
itself only for an approval made through serve, and only when auto-resume
applies — see Auto-resume. A local swamp workflow approve
never triggers it.
The suspend, approve, reject, and resume lifecycle is described in Manual Approval Lifecycle. For the step-by-step procedure, see the how-to guide Gate a Workflow with Manual Approval.
Assert (assert)
Evaluates a CEL expression and records whether it passed or failed. Assert steps run after data-producing steps and verify that the collected state meets expectations.
task:
type: assert
expr: data.latest("server-check", "result").attributes.exitCode == 0
message: "Server exited cleanly"
severity: high| Field | Type | Required | Description |
|---|---|---|---|
type |
"assert" |
Yes | Task type discriminator |
expr |
string (CEL expression) | Yes | Expression to evaluate. Truthy result = passed; falsy = failed. |
message |
string | Yes | Human-readable message. Supports ${{ }} expression interpolation. |
severity |
"low" | "medium" | "high" |
No | Controls the --fail-on threshold. Defaults to "high" if omitted. |
expr is a raw CEL expression — do not wrap it in ${{ }}. The expression has
access to the full step expression context (data, inputs, self, model,
env, vault, file), including
model.method()
calls for checking live state.
message supports ${{ }} interpolation. If an interpolated expression inside
the message fails to evaluate, the expression text is left as-is.
message: >-
Expected at least 3 instances, got
${{ size(data.latest("fleet", "nodes").attributes.instances) }}Severity and --fail-on
Each assert carries a severity. The --fail-on flag on swamp workflow run
sets the threshold — only failures at or above the threshold cause the workflow
run to fail. Failures below the threshold are recorded but do not affect the
exit code.
| Assert severity | --fail-on low |
--fail-on medium |
--fail-on high |
|---|---|---|---|
low |
Fails run | Recorded only | Recorded only |
medium |
Fails run | Fails run | Recorded only |
high |
Fails run | Fails run | Fails run |
The default --fail-on value is low — any failed assertion fails the run.
A failed assert step is treated as an allowed failure when either the step
has allowFailure: true or its severity is below the --fail-on threshold.
Assert result
Each assert step produces an assertResult on its step run record:
{
"passed": true,
"expr": "data.latest(\"server-check\", \"result\").attributes.exitCode == 0",
"message": "Server exited cleanly",
"severity": "high"
}| Field | Type | Description |
|---|---|---|
passed |
boolean | Whether the expression evaluated to truthy. |
expr |
string | The original CEL expression. |
message |
string | The resolved message (after interpolation). |
severity |
string | The severity level (low, medium, or high). |
assertResult is visible in --json output from swamp workflow run and in
swamp workflow history get --json.
JUnit XML output
The --junit flag on swamp workflow run emits assert results as JUnit XML.
Each assert step becomes a <testcase>; non-assert steps are omitted. Passing
assertions are self-closing elements; failures include the resolved message and
the raw CEL expression.
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="infra-verify" tests="3" failures="2" time="0.1">
<testsuite name="verify" tests="3" failures="2" time="0.0">
<testcase name="check-healthy" classname="infra-verify.verify" time="0.0"/>
<testcase name="check-count" classname="infra-verify.verify" time="0.0">
<failure message="Expected count of 3 in output" type="AssertionFailure">
severity: medium
expr: data.latest("server-check", "result").attributes.stdout.contains(...)
</failure>
</testcase>
</testsuite>
</testsuites>Use --out <file> to write the XML to a file instead of stdout. --out
requires --junit. --junit and --json cannot be combined.
For the step-by-step procedure, see the how-to guide Verify Infrastructure with Assert Steps.
ForEach
Expands a step into multiple executions, one per element in the evaluated collection.
forEach:
item: env
in: "${{ inputs.environments }}"| Field | Type | Required | Description |
|---|---|---|---|
item |
string | Yes | Variable name bound to each element |
in |
string | Yes | CEL expression evaluating to a list or map |
When iterating over a list, self.{item} is set to each element.
When iterating over a map, self.{item} is set to an object with key and
value fields for each entry.
self._index is the zero-based position of the current iteration: list elements
in order, map entries in key order. An iteration variable named _index shadows
it. In a CEL expression that concatenates it with a string, convert it first:
string(self._index) + "-" + self.file.
Expanded steps inherit the original step's dependencies. If the step name
contains ${{ }} expressions, they are evaluated with the iteration context to
produce unique step names. Otherwise, the iteration value or index is appended
as a suffix (e.g., deploy-env-staging, deploy-env-production).
The iteration variable (self.{item}) resolves in modelIdOrName,
methodName, step name, and task inputs — not just in inputs. This
enables forEach steps to target different model instances per iteration without
requiring a nested workflow:
steps:
- name: summary-${{ self.region }}
forEach:
item: region
in: "${{ inputs.regions }}"
task:
type: model_method
modelIdOrName: aws-alarms-${{ self.region }}
methodName: get_summary
inputs:
historyHours: 24Each iteration resolves self.region to the current element, producing steps
like summary-us-east-1 that target aws-alarms-us-east-1, summary-eu-west-1
that target aws-alarms-eu-west-1, and so on.
To give each iteration a model definition of its own, build modelName from
run.id and self._index. The name is unique per run and per iteration even
when two elements are equal:
steps:
- name: process-${{ self._index }}
forEach:
item: path
in: "${{ inputs.files }}"
task:
type: model_method
modelType: command/shell
modelName: processor-${{ run.id }}-${{ self._index }}
methodName: execute
globalArgs:
run: "wc -l ${{ self.path }}"With files: ["a.txt", "b.txt"], the steps process-0 and process-1 create
and run processor-<run id>-0 and processor-<run id>-1.
Trigger Conditions
Trigger conditions control when a job or step executes relative to its
dependencies. They are specified in the condition field of dependsOn
entries.
Leaf Conditions
| Type | Evaluates to true when |
|---|---|
always |
Always (unconditional) |
succeeded |
The dependency succeeded |
failed |
The dependency failed |
completed |
The dependency completed (succeeded or failed) |
skipped |
The dependency was skipped |
dependsOn:
- job: build
condition:
type: succeededComposite Conditions
Leaf conditions can be composed with boolean logic.
and — all child conditions must be true. Requires at least 2 conditions.
condition:
type: and
conditions:
- type: succeeded
- type: not
condition:
type: skippedor — any child condition must be true. Requires at least 2 conditions.
condition:
type: or
conditions:
- type: succeeded
- type: failednot — inverts a single condition.
condition:
type: not
condition:
type: failedComposite conditions nest recursively.
DataOutputOverride
Overrides data output specifications for data produced by a step's task.
| Field | Type | Required | Description |
|---|---|---|---|
specName |
string | Yes | Output spec name to override |
lifetime |
Lifetime |
No | Override data retention |
garbageCollection |
GarbageCollectionPolicy |
No | Override version retention |
tags |
Record<string, string> |
No | Additional tags to merge with output tags |
vary |
string[] |
No | Input key names to vary by (composite data names) |
Lifetime
How long data is retained.
| Value | Description |
|---|---|
| Duration string | 1h, 5m, 10d, 2w, 1mo, 10y |
ephemeral |
Deleted when the process ends |
infinite |
Never automatically deleted |
job |
Lives until the job completes |
workflow |
Lives until the workflow completes |
Duration format: {number}{unit} where unit is h (hours), m (minutes), d
(days), w (weeks), mo (months), or y (years). Zero durations (e.g., 0h)
are normalized to workflow.
GarbageCollectionPolicy
How many versions to retain.
| Value | Description |
|---|---|
| integer | Keep N most recent versions |
| Duration string | Keep versions created within the duration |
dataOutputOverrides:
- specName: result
lifetime: 30d
garbageCollection: 5
tags:
stage: deploy
vary:
- environmentCEL Expressions
String values in step task inputs, modelIdOrName, methodName, step name,
and forEach.in fields support CEL expressions using the ${{ }} wrapper.
task:
type: model_method
modelIdOrName: deployer
methodName: apply
inputs:
region: "${{ inputs.region }}"
artifact: "${{ model.builder.resource.result.result.attributes.path }}"
secret: "${{ vault.get('infra', 'deploy-key') }}"Context Variables
| Variable | Description |
|---|---|
self |
The current model definition: self.name, self.tags.*, self.globalArguments.* |
model |
All models: model.<name>.definition.*, model.<name>.resource.<spec>.<instance>.attributes.* |
inputs |
Workflow or model runtime inputs: inputs.<property> |
env |
Process environment variables: env.<VAR_NAME> |
vault |
Secrets: vault.get('<vault>', '<key>') |
data |
Versioned data: data.latest(...), data.version(...), data.findBySpec(...), data.query(...) |
file |
File contents: file.contents('<model>', '<spec>') |
When forEach is active, the iteration variable is available as self.{item}
and the zero-based iteration index as self._index. For example, with
forEach: { item: "env", in: [...] }, each iteration sets self.env to the
current element.
After a step completes, its model's execution data and output data are available
to subsequent steps through the model context. For example,
model.builder.execution.status and
model.builder.resource.result.result.attributes.*.
See the CEL Expressions reference for the full expression language.
Execution Order
Jobs and steps are sorted topologically using Kahn's algorithm with weighted tie-breaking.
- Dependency level — nodes are grouped into levels based on the dependency graph. Level 0 has no dependencies, level 1 depends only on level 0, etc.
- Weight — within the same level, lower weights execute first.
- Name — within the same level and weight, names are sorted alphabetically for determinism.
Nodes at the same level with no mutual dependencies execute in parallel.
Cyclic dependencies are detected and produce a validation error with the cycle
path (e.g., job-a -> job-b -> job-a).
Manual Approval Lifecycle
A manual_approval step pauses execution until an approval decision is
recorded. While the gate is pending, the run holds at the gate and no downstream
step starts.
Run and step statuses
When a run reaches a manual_approval step, the workflow run status becomes
suspended and the gate step status becomes waiting_approval. Steps that ran
before the gate keep their terminal status (succeeded); steps after the gate
stay pending.
| Status | Applies to | Meaning |
|---|---|---|
suspended |
Run | The run is paused at a manual_approval step. |
waiting_approval |
Step | The gate is awaiting an approval decision. |
interrupted |
Run | The run was terminated before completion (e.g., process crash, a force-exited owner, a swamp serve restart). |
unknown |
Step | The step's outcome is not known — the run was interrupted mid-step. |
An approval transitions the gate step to succeeded. A rejection transitions
the gate step to failed and the run to failed; downstream steps remain
pending.
A run that is still suspended after every gate on it is approved is awaiting
resume. swamp workflow run search --json marks it with awaitingResume: true,
and the dashboard
lists it with a Resume action.
Auto-resume
By default an approved run stays suspended until someone runs
swamp workflow resume. With auto-resume, serve resumes the run itself when an
approval made through serve — the dashboard or swamp workflow approve --server
— decides the run's last gate.
Whether a run auto-resumes is decided per workflow:
Workflow autoResume |
Workflow declares inputs | Serve --auto-resume |
Auto-resumes |
|---|---|---|---|
true |
either | either | Yes |
false |
either | either | No |
| unset | No | on | Yes |
| unset | No | off (default) | No |
| unset | Yes | either | No |
A workflow declares inputs when its inputs schema has at least one property or
allows additional properties. Serve's --auto-resume flag (SWAMP_AUTO_RESUME,
config key auto-resume) is described in
Serve Flags.
An auto-resume supplies no new inputs; the run resumes with its original inputs. The resume counts against the approver's principal for serve's concurrent-run limits, and the run keeps its original initiator.
Serve audits the launch as workflow.auto_resume. If the launch is refused or
the resume fails, serve logs and audits workflow.auto_resume_failed, and the
run stays suspended, awaiting resume.
A local swamp workflow approve never triggers auto-resume, even against a
repository that a serve instance also uses.
Checkpoint recovery
When a run is interrupted (process crash, node failure, or forced termination),
the platform persists a durable checkpoint of completed step results. The run
status becomes interrupted and any in-flight step becomes unknown.
To recover an interrupted run, run swamp workflow recover, then
swamp workflow resume:
swamp workflow recover deploy-pipeline
swamp workflow resume deploy-pipelineThe platform reloads the checkpoint, skips steps that completed before the
interruption, and re-runs the unknown step from the beginning. Guard
expressions on completed steps provide an additional safety net — even without a
checkpoint, a guarded step that already wrote its data will be skipped on
re-execution.
Steps that were pending (not yet started) at the time of the interruption
execute normally after the recovered step succeeds.
Runs left running by a force-exited process
A run whose owning process was force-exited — a second Ctrl+C during
swamp workflow run or swamp workflow resume, or a SIGKILL — is left
recorded as running. A step's running status is saved when the step starts,
so a step that was mid-flight is settled as unknown, not pending.
Either command settles the run record: the run becomes interrupted with
interrupt_reason: owner_process_dead, and its in-flight steps become
unknown.
| Command | Effect |
|---|---|
swamp workflow recover <workflow> --run <id> |
Settles that run and recovers it, ready for swamp workflow resume |
swamp run doctor --fix |
Settles every workflow run on this host whose owner is gone; each still needs swamp workflow recover before resume |
swamp workflow resume on a run in this state refuses and names the recover
command:
Error: No suspended runs found for workflow "slow". The latest run (faebb8ba-cb7b-43a2-90d7-d39b1dfd2db5) is recorded as running, but its owning process is gone. Recover it with 'swamp workflow recover slow --run faebb8ba-cb7b-43a2-90d7-d39b1dfd2db5', then resume it.An unknown step without a guard expression needs --acknowledge-unknown
before recovery re-runs it:
Error: Cannot auto-recover: 1 unknown step(s) lack guard expressions — operator acknowledgement required
Unguarded steps: nap
Use --acknowledge-unknown to accept re-execution risk.See Make Workflows Idempotent for how to combine checkpoint recovery with guard expressions.
CLI Commands
Workflow management commands for the Swamp CLI.
swamp workflow create <name>
Create a new workflow file.
| Argument | Required | Description |
|---|---|---|
name |
Yes | Workflow name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
swamp workflow run <name>
Execute a workflow. The command blocks the calling process until the run reaches
a terminal state — succeeded, failed, or cancelled — or until the run
suspends at a manual_approval gate. There is no async or detached mode on the
CLI; on swamp serve, HA mode auto-detects when the datastore supports a
control plane and detaches runs from the client connection automatically (see
Serve Flags). The exit code
reflects the run outcome (0 for success, non-zero otherwise). To abort a
blocking run, press Ctrl+C (SIGINT) or send SIGTERM — see
Execution Cancellation.
| Argument | Required | Description |
|---|---|---|
name |
Yes | Workflow name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--input |
Set an input value (key=value). Repeatable. |
--input-file |
YAML file of input values. |
--stdin |
Read input values from standard input. |
--tag |
Tag the run (key=value). Repeatable. |
--skip-checks |
Skip all checks. |
--skip-check |
Skip a check by name. Repeatable. |
--skip-check-label |
Skip checks matching a label. Repeatable. |
--skip-reports |
Skip all reports. |
--skip-report |
Skip a report by name. Repeatable. |
--skip-report-label |
Skip reports matching a label. Repeatable. |
--report |
Run a specific report by name. Repeatable. |
--report-label |
Run reports matching a label. Repeatable. |
--timeout |
Maximum run duration. At most about 24.8 days. |
--server |
Target a remote swamp serve instance. |
--token |
Authentication token for the remote server. |
--fail-on |
Assert severity threshold (low, medium, high). Default: low. Not supported with --server. |
--junit |
Output assert results as JUnit XML instead of normal log output. |
--out |
Write JUnit XML to a file instead of stdout. Requires --junit. |
--last-evaluated |
Reuse the previously evaluated workflow and definitions. See below. |
--last-evaluated
When set, swamp workflow run skips the full CEL evaluation pass and loads the
previously evaluated workflow from .swamp/workflows-evaluated/. This avoids
re-reading model definitions from disk and re-evaluating expressions that have
not changed.
Deferred data expressions (data.query(), data.findByTag(), data.latest(),
data.version(), data.listVersions(), data.findBySpec()) inside step
task.inputs and task targets are still resolved at step execution time. The
engine builds a lightweight expression context containing only the data and
env namespaces, which is sufficient because the only unevaluated expressions
remaining are data function calls that reference upstream step outputs.
swamp workflow get <name>
Show a workflow definition.
| Argument | Required | Description |
|---|---|---|
workflow_id_or_name |
Yes | Workflow ID or name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--graph |
Render the workflow's dependency graph as ASCII art. Shows a job-level DAG when jobs have inter-job dependencies, and step-level DAGs per job when steps have inter-step dependencies. |
--server |
Target a remote swamp serve instance. |
--token |
Authentication token for the remote server. |
When --graph is passed, the output includes box-drawing diagrams with proper
edge routing for fan-out/fan-in patterns. Step graphs label the task type
(model, workflow, approval, assert). A single job with no step
dependencies omits the step graph.
Workflow: diamond-pipeline (5 jobs, 5 steps)
Jobs:
┌────────┐
│ fetch ├───────┐
└────┬───┘ │
▼ ▼
┌────────┐ ┌─────────┐
│validate│ │transform│
└────┬───┘ └────┬────┘
▼ │
┌────────┐ │
│ merge │◄──────┘
└────┬───┘
▼
┌────────┐
│ report │
└────────┘In --json output, each workflow includes a trigger field when a trigger is
configured:
{
"id": "d14de37a-b931-4363-97e9-fe9ec9637b60",
"name": "deploy-pipeline",
"description": "A multi-stage deployment pipeline",
"trigger": {
"schedule": "0 3 * * *",
"inputs": {
"projectId": "a6b254a2-0b57-4d0f-bf8b-fef767ab119e"
}
},
"tags": {
"team": "platform"
},
"jobs": ["..."],
"version": 1
}trigger is omitted when the workflow has no trigger configured.
swamp workflow search [query]
Search workflows.
| Argument | Required | Description |
|---|---|---|
query |
No | Search query. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--server |
Target a remote swamp serve instance. |
--token |
Authentication token for the remote server. |
--json output is an object with the query and a results array of workflow
summaries. The same shape is returned with --server:
{
"query": "",
"results": [
{
"id": "5725fbde-c789-43eb-9eaf-70afb8d10b97",
"name": "gated-deploy",
"description": "Build, wait for sign-off, then deploy",
"jobCount": 2,
"stepCount": 4,
"hasInputs": false
},
{
"id": "0405ef03-5ccf-4f90-9361-bf461ddf52dd",
"name": "nightly-report",
"jobCount": 1,
"stepCount": 1,
"hasInputs": false,
"trigger": {
"schedule": "0 3 * * *"
}
}
]
}| Field | Type | Description |
|---|---|---|
id |
string | Workflow ID |
name |
string | Workflow name |
description |
string | Workflow description. Omitted when not set. |
jobCount |
number | Number of jobs |
stepCount |
number | Number of steps across all jobs |
hasInputs |
boolean | Whether the workflow declares inputs |
trigger |
object | schedule and inputs of the workflow trigger. Omitted when not set. |
Search results are summaries; version, tags and jobs are returned by
swamp workflow get.
swamp workflow edit [name]
Open a workflow in $EDITOR, or replace it with YAML piped on stdin.
| Argument | Required | Description |
|---|---|---|
workflow_id_or_name |
No | Workflow ID or name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL). Takes the new YAML on stdin only. |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
cat deploy.yaml | swamp workflow edit deployWith --server, stdin is the only edit mode, because the editor cannot run on
the server. Running it with nothing piped fails:
'swamp workflow edit' with --server needs the new YAML piped on stdin, because the editor cannot run on the server. For example: cat edited.yaml | swamp workflow edit <name> --server <url>A workflow whose stored YAML no longer parses must be fixed in an editor first.
swamp workflow delete <name>
Delete a workflow, its run history, and each deleted run's evaluated-workflow
snapshot (.swamp/workflows-evaluated/runs/<run-id>/). A per-run snapshot
shares its run's lifetime: it is removed when the run is, here or by
swamp run gc.
| Argument | Required | Description |
|---|---|---|
workflow_id_or_name |
Yes | Workflow ID or name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
-y, --yes |
Skip confirmation prompt (--force / -f also accepted). |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
swamp workflow validate [name]
Validate a workflow's schema, dependencies, and method resolution.
| Argument | Required | Description |
|---|---|---|
workflow_id_or_name |
No | Workflow ID or name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
Omitting the name validates all workflows.
swamp workflow evaluate [name]
Test CEL expressions in a workflow without executing it.
| Argument | Required | Description |
|---|---|---|
workflow_id_or_name |
No | Workflow ID or name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--all |
Evaluate all workflows. |
--input |
Set an input value (key=value). Repeatable. |
--input-file |
YAML file of input values. |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
swamp workflow schema get
Print the YAML schema for workflow files.
| Flag | Description |
|---|---|
--server |
Target a remote swamp serve instance. |
--token |
Authentication token for the remote server. |
swamp workflow cancel [name]
Cancel a running workflow.
| Argument | Required | Description |
|---|---|---|
workflow_id_or_name |
No | Workflow ID or name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--all |
Cancel all locally-owned running workflows. Serve-owned runs are skipped with a warning. Not supported with --server. |
--run |
Target a specific run ID. Required with --server. |
--reason |
Cancellation reason for the audit trail. |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
swamp workflow approve <name> <step>
Approve a manual_approval step in a suspended run. Approval records the
decision; it does not run anything. See
Manual Approval Lifecycle.
| Argument | Required | Description |
|---|---|---|
workflow_id_or_name |
Yes | Workflow ID or name. |
step_name |
Yes | Name of the manual_approval step. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--reason |
Reason for the approval. |
--run |
Target a specific run ID. Optional when only one run is suspended. |
--server |
Approve through a remote swamp serve instance (env: SWAMP_SERVE_URL or SWAMP_SERVER_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
--token-file |
Path to a file containing the server token; mutually exclusive with --token (env: SWAMP_SERVER_TOKEN_FILE) |
A local approval prints the resume command to run next:
Approved step "approve-prod" in workflow "gated-deploy"
After approval: swamp workflow resume gated-deploy --run d1fe506b-fdc4-40e0-8764-fc0247c1b51dWith --server, when the approval decides the run's last gate and
auto-resume applies, serve resumes the run and the command
prints:
Approved step "approve-prod" in workflow "gated-deploy"
Serve is resuming run "7ffa1968-db04-4c4f-bf87-a1bd2c640222" automatically--json output looks like:
{
"runId": "d1fe506b-fdc4-40e0-8764-fc0247c1b51d",
"workflowName": "gated-deploy",
"stepName": "approve-prod",
"approved": true,
"decidedBy": "user:adam",
"reason": null,
"allGatesDecided": true,
"autoResumed": true
}| Field | Type | Description |
|---|---|---|
runId |
string | The approved run |
workflowName |
string | Workflow name |
stepName |
string | The approved step |
approved |
boolean | Always true |
decidedBy |
string | Who approved: the authenticated principal through serve; locally $USER or $USERNAME, else unknown |
reason |
string | null | The --reason value |
allGatesDecided |
boolean | Whether this approval decided the run's last pending gate |
autoResumed |
boolean | Whether serve is resuming the run itself. Present only with --server. |
When autoResumed is false, run swamp workflow resume to continue the run.
swamp workflow trigger set <name>
Set a trigger override for a workflow in serve.yaml. Each call writes the
complete override entry — it replaces the previous override, not a partial
merge.
| Argument | Required | Description |
|---|---|---|
name |
Yes | Workflow name. |
| Flag | Description |
|---|---|
--schedule |
Cron expression for the trigger schedule (required). |
--input |
Input value (key=value). Repeatable. |
--repo-dir |
Path to the Swamp repository. |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp workflow trigger set nightly-report --schedule "0 3 * * *"
Set trigger override for "nightly-report": schedule "0 3 * * *"$ swamp workflow trigger set nightly-report --schedule "0 3 * * *" --input env=production
Set trigger override for "nightly-report": schedule "0 3 * * *"
inputs: "env=production"swamp workflow trigger get <name>
Show the effective trigger for a workflow. Displays the built-in trigger (from
the workflow YAML), the override (from serve.yaml), and the effective (merged)
trigger.
| Argument | Required | Description |
|---|---|---|
name |
Yes | Workflow name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp workflow trigger get nightly-report
Trigger for "nightly-report":
built-in:
schedule: "0 0 * * *"
override (serve.yaml):
schedule: "0 3 * * *"
input "env": "production"
effective:
schedule: "0 3 * * *" ("override")
input "env": "production" ("override")In --json output:
{
"workflowName": "nightly-report",
"builtIn": {
"schedule": "0 0 * * *",
"inputs": {}
},
"override": {
"schedule": "0 3 * * *",
"inputs": {
"env": "production"
}
},
"effective": {
"schedule": "0 3 * * *",
"inputs": {
"env": "production"
}
}
}swamp workflow trigger remove <name>
Remove a trigger override for a workflow from serve.yaml. The workflow reverts
to its built-in trigger (if any).
| Argument | Required | Description |
|---|---|---|
name |
Yes | Workflow name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--server |
Run against a remote swamp serve instance (env: SWAMP_SERVE_URL) |
--token |
Server token in <name>.<secret> format; only with --server (overrides stored credentials and SWAMP_SERVER_TOKEN) |
$ swamp workflow trigger remove nightly-report
Removed trigger override for "nightly-report"Approval decision record
When a gate is approved or rejected, an approvalDecision is written to the
gate's step run record.
| Field | Type | Description |
|---|---|---|
approved |
boolean | true for an approval, false for a rejection. |
reason |
string | The --reason text, when provided. Omitted otherwise. |
decidedBy |
string | The authenticated identity that made the decision. |
decidedAt |
string | ISO 8601 timestamp of the decision. |
Commands
| Command | Effect |
|---|---|
swamp workflow approvals |
Lists runs suspended at a gate and still within their approval window, with the workflow, step, and prompt. Runs whose timeout has elapsed are omitted — they can no longer be approved. |
swamp workflow approve <workflow> <step> |
Records an approval on the gate. Accepts --reason <text>. Does not run downstream steps. |
swamp workflow reject <workflow> <step> |
Records a rejection and marks the run failed. Accepts --reason <text>. |
swamp workflow resume <workflow> |
Resumes a suspended run once its gates are decided, skipping already-completed steps. With --run <id>, retries the failed steps of a failed run — see Retry failed steps. Accepts --from <step> to re-enter a failed run at a specific step — see Resume from a specific step. Accepts override inputs via --input, --input-file, or --stdin — see Resume inputs. Accepts --timeout — see Resume timeout. |
All four commands accept --server and --token to target a remote
swamp serve instance.
approve and reject check the approve action on the workflow resource, not
run. A user with only run cannot approve or reject a gate unless run
implicitly permits approve (which it does by default — see
Run implies approve).
To enforce separation of duties, deny approve on runners and grant it to a
dedicated approver role.
approve records the decision but does not execute downstream work; resume
performs the execution. Resuming a suspended run whose gate has not been decided
fails with an error naming the step to approve first. resume also continues a
failed run: --run <id> alone retries its failed steps — see
Retry failed steps — and --from re-enters it at a step
you choose — see Resume from a specific step.
See
Understanding Workflow Suspension
for why approval and resume are separate operations.
Resume inputs
swamp workflow resume accepts override inputs through three flags:
| Flag | Source |
|---|---|
--input <key=value> |
A single key set on the command line. The value may be a scalar or JSON. Repeatable. |
--input-file <file> |
A YAML file of override inputs. |
--stdin |
Override inputs read as piped data from standard input. |
--input-file and --stdin cannot be combined. --input may accompany either.
Override inputs deep-merge over the original run inputs. On a key collision the resume value wins; keys not present in the original run are added. Nested objects merge key by key rather than replacing the whole object.
The run record lists the names of the keys supplied at resume in a
resumeInputs list. Once merged, resume values are ordinary input values: they
are persisted in the same places as run inputs (see
Input values are inert and recorded),
including the run's inputs if it suspends again. Never supply a secret value
at resume. Store it with swamp vault put while the run is suspended and supply
only its key name. See
Gate a Workflow with Manual Approval
for the task and
Understanding Workflow Suspension
for the rationale.
swamp workflow resume deploy-with-gate --input secretKey=deploy-auth-key-42Resume timeout
swamp workflow resume accepts --timeout to set a maximum run duration,
matching the same flag on swamp workflow run. The value is bare seconds (30,
1800) or a duration string (30s, 5m, 1h). When omitted, the resumed run
executes unbounded.
swamp workflow resume deploy-with-gate --timeout 5mRetry failed steps
swamp workflow resume <workflow> --run <id> retries the failed steps of a
failed run. Every failed step and every step downstream of it runs again, in
the same run. Steps that succeeded and do not depend on a failed step keep their
results and do not run again. The run keeps its ID and start time.
swamp workflow resume deploy-pipeline --run 3f2a9c1e-5b7d-4e8a-9c0f-1d2e3f4a5b6cWhen a run fails, swamp workflow run prints this command, including the
--server or --repo-dir the run used. The server URL is printed without
credentials.
--run is required. Without it, swamp workflow resume matches only a
suspended run.
A retry resets these steps to pending:
- each failed step whose failure was not allowed by
allowFailure; - for a failed
forEachiteration, every iteration of its step; - every step that depends on one of those steps, in the same job or through a
job's
dependsOn.
Reset steps lose their earlier outputs. A reset step's guard still
decides whether it runs. --input overrides merge over the original inputs as
for any resume, but do not reset steps that already used the old value.
A retry is refused, without changing the run, when:
| Condition | What to do instead |
|---|---|
A failed step is a rejected manual_approval gate. |
--from <gate> resets the gate, which asks again. |
A step is pending, running, waiting_approval, or unknown. |
For a pending step, --from <step>. |
| A job failed before any of its steps failed. | Read the run log with swamp workflow history logs <id>. |
| A failed step was renamed, removed, or moved to another job. | Start a new run. |
| A step name is used in more than one job. | Start a new run. |
Every refusal names the job or step and the swamp workflow history logs
command for the run.
A retry does not guarantee exactly-once execution. A step that changed an
external system and then failed runs again. Steps downstream of a failed step
run again even if they succeeded, including cleanup steps. A retry uses the
current workflow and model definitions; if the workflow or a forEach
collection changed since the run, start a new run. A retried nested workflow
starts a new child run.
Resume from a specific step
swamp workflow resume accepts --from <step> to re-enter a failed run's DAG
at a named step you choose, after fixing the cause of failure. A suspended run
continues with a plain swamp workflow resume once its gates are decided.
| Flag | Description |
|---|---|
--from <step> |
Step template name to resume from, as written in the YAML file. |
The --from value is the step's template name — the name field as it
appears in the workflow definition, before any forEach expansion. For a
forEach step named deploy-${{ self.env }}, pass the template
deploy-${{ self.env }}, not an expanded name like deploy-staging.
Execution behaviour
Steps that completed successfully before the failure are not re-run — this
prevents repeating irreversible side effects. The --from step and all steps
downstream of it in the dependency graph are eligible for execution.
For forEach steps, the platform re-expands all iterations from the template.
Each iteration's guard expression decides whether it executes or is
skipped. This means completed iterations are skipped (their guard sees existing
data) while the failed iteration re-runs.
swamp workflow resume provision-stack --from deploy-$\{\{ self.env \}\}Combining with guards
--from and guard work together for safe recovery:
- A workflow run fails at step 3 of 5.
- You fix the underlying problem.
- You resume with
--fromtargeting step 3. - Steps 1 and 2 are not re-run (already completed).
- Step 3 re-runs because its guard sees no completion data for the failed attempt.
- Steps 4 and 5 proceed as normal.
For a goal-oriented walkthrough, see Make Workflows Idempotent — Recover a failed run.
Execution Cancellation
A running workflow can be cancelled via swamp workflow cancel. Cancellation
terminates in-flight steps cooperatively and marks the run as cancelled — a
terminal status alongside succeeded and failed.
cancelled status
| Status | Applies to | Meaning |
|---|---|---|
cancelled |
Run | The run was cancelled before completing. |
cancelled |
Step | The step was cancelled before completing. |
Steps that completed before the cancel signal keep their terminal status
(succeeded or failed). Steps that were running or pending at the time of
cancellation transition to cancelled.
Commands
| Command | Effect |
|---|---|
swamp workflow cancel <workflow> |
Cancel the latest running run of the named workflow. |
swamp workflow cancel <workflow> --run <run-id> |
Cancel a specific run by ID. |
swamp workflow cancel --all |
Cancel all locally-owned running runs. Serve-owned runs are skipped with a warning. |
swamp workflow cancel <workflow> --reason <text> |
Cancel with an audit-trail reason recorded on the run. |
Cancellation with --server
When cancelling via --server, the cancel endpoint distinguishes two outcomes:
cancelled— the run was confirmed stopped (deregistered within the grace period).cancellation_requested— the abort signal was delivered but the run is still active. The run may stop on its own once the signal is processed.
The bulk cancel endpoint (POST /api/v1/cancel) always returns
cancellation_requested, since it delivers abort signals without waiting for
confirmation. See the
REST API cancel endpoints
for the full response schemas.
Locally-owned vs. serve-owned runs
swamp workflow cancel --all only cancels runs owned by the local process. Runs
owned by a swamp serve instance are skipped with a warning — this prevents a
local --all from inadvertently stopping server-managed workloads.
To cancel a serve-owned run, target the server directly with --server and
--run:
swamp workflow cancel my-workflow --server wss://swamp.example.com --run 3f8a2b1c--all is not supported with --server. To cancel all runs on a server, use
the
bulk cancel REST endpoint.
Signal-based cancellation
Pressing Ctrl+C (SIGINT) or sending SIGTERM during swamp workflow run cancels
the running workflow. The CLI intercepts the signal, sends a cancel request,
waits for in-flight steps to terminate cooperatively, and exits with a non-zero
status.
Examples
$ swamp workflow cancel my-workflow
$ swamp workflow cancel my-workflow --run 3f8a2b1c
$ swamp workflow cancel --all
$ swamp workflow cancel my-workflow --reason 'No longer needed'
$ swamp workflow cancel my-workflow --server wss://swamp.example.com --run 3f8a2b1cConcurrency Resolution Order
When steps within a job execute in parallel, the effective concurrency limit is resolved from the most specific level:
- Step
concurrency— if multiple steps at the same dependency level specifyconcurrency, the minimum value across those steps is used. - Job
concurrency - Workflow
concurrency - Unbounded
The first non-zero value at each level wins.
Job-level parallelism follows the same pattern: workflow concurrency is
applied across jobs at the same dependency level.
SWAMP_MAX_CONCURRENT_STEPS
Environment variable that sets a host-level ceiling on all concurrency values. When set to a positive integer, the effective concurrency at every level is capped to this value regardless of what the workflow, job, or step specifies. When unset or not a positive integer, no host-level cap is applied.
SWAMP_MAX_CONCURRENT_STEPS=4Validation
swamp workflow validate checks a workflow against its schema:
$ swamp workflow validate multi-stage
Validating: multi-stage
✓ Schema validation
✓ Unique job names
✓ Unique step names in job 'build'
✓ Unique step names in job 'deploy'
✓ Unique step names in job 'notify'
✓ Valid job dependency references
✓ Valid step dependency references in job 'build'
✓ Valid step dependency references in job 'deploy'
✓ Valid step dependency references in job 'notify'
✓ No cyclic job dependencies
✓ No cyclic step dependencies in job 'build'
✓ No cyclic step dependencies in job 'deploy'
✓ No cyclic step dependencies in job 'notify'
Summary: 13/13 validations passed
Result: PASSEDOmit the name to validate all workflows in the repository:
$ swamp workflow validate
Validating all workflows...
multi-stage
✓ Schema validation
...
Summary: 1/1 workflows passed
Overall: PASSEDValidation rules:
idmust be a valid UUID v4.namemust be at least 1 character, unique within the repository, with no path traversal sequences.versionmust be a positive integer.concurrencymust be a non-negative integer when present (at workflow, job, or step level).tagsvalues must be strings.trigger.schedulemust be a valid cron expression.jobsmust contain at least one job.- Job names must be unique within the workflow.
- Step names must be unique within each job.
- Each job must contain at least one step.
- Job dependency references must name existing jobs.
- Step dependency references must name existing steps within the same job.
- No cyclic dependencies among jobs.
- No cyclic dependencies among steps within each job.
${{ inputs.* }}expressions in model definitionglobalArgumentsmust reference inputs declared in the workflow'sinputsblock.- Unknown keys on workflows, jobs, and steps are rejected. A typo produces a did-you-mean suggestion naming the closest valid key.
- Placement fields (
target,labels,platform,queueTimeout) are valid at the workflow, job, and step level. See Workflow Placement for the inheritance chain and scheduling behavior. affinityis valid at the workflow and job level. A validation warning is emitted whenaffinity: trueis set but no placement field (target,labels, orplatform) is effective — affinity without placement is a no-op.
Unknown-key rejection applies at both swamp workflow validate and load time
(swamp workflow run, swamp workflow evaluate). Workflow files that
previously loaded with unrecognised keys now fail. Run
swamp doctor workflows to
identify affected files in a repository.
Complete Example
id: d14de37a-b931-4363-97e9-fe9ec9637b60
name: multi-stage
description: A multi-stage deployment pipeline
trigger:
schedule: "0 3 * * *"
tags:
team: platform
env: production
inputs:
type: object
properties:
environments:
type: array
description: Target environments
region:
type: string
default: us-east-1
required:
- environments
labels:
pool: gke
platform: linux
jobs:
- name: build
description: Compile and test
weight: 0
steps:
- name: compile
description: Build the artifacts
task:
type: model_method
modelIdOrName: builder
methodName: run
inputs:
target: "${{ inputs.region }}"
weight: 0
allowFailure: false
- name: test
description: Run tests
task:
type: model_method
modelIdOrName: test-runner
methodName: execute
dependsOn:
- step: compile
condition:
type: succeeded
weight: 1
allowFailure: false
- name: deploy
description: Deploy to each environment
dependsOn:
- job: build
condition:
type: succeeded
weight: 10
concurrency: 2
steps:
- name: deploy-env
description: Deploy to environment
guard: "${{ data.latest('deployer', 'result') }}"
task:
type: model_method
modelIdOrName: deployer
methodName: apply
forEach:
item: env
in: "${{ inputs.environments }}"
dataOutputOverrides:
- specName: result
lifetime: 30d
garbageCollection: 5
tags:
stage: deploy
vary:
- environment
- name: notify
description: Send notifications
labels: {}
dependsOn:
- job: deploy
condition:
type: completed
weight: 20
steps:
- name: send-notification
task:
type: workflow
workflowIdOrName: notification-workflow
inputs:
channel: "#deployments"
allowFailure: true
version: 1
reports:
require:
- name: deployment-report
methods:
- apply
- summary
skip:
- debug-reportHistory
Commands for inspecting past workflow runs.
swamp workflow history get <name>
Show the latest run for a workflow.
| Argument | Required | Description |
|---|---|---|
workflow_id_or_name |
Yes | Workflow ID or name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--server |
Target a remote swamp serve instance. |
--token |
Authentication token for the remote server. |
swamp workflow history search [query]
Search past workflow runs.
| Argument | Required | Description |
|---|---|---|
query |
No | Search query. |
| Flag | Description |
|---|---|
--filter <expression> |
Filter results with a CEL expression over run metadata. |
--repo-dir |
Path to the Swamp repository. |
--server |
Target a remote swamp serve instance. |
--token |
Authentication token for the remote server. |
--filter
The --filter flag accepts a CEL expression evaluated against each run's
metadata. Only runs where the expression evaluates to a truthy value are
returned.
Available fields:
| Field | Type | Description |
|---|---|---|
workflowName |
string |
Name of the workflow. |
status |
string |
Run status (succeeded, failed, cancelled, …). |
startedAt |
string |
ISO 8601 timestamp (lexicographic comparison works). |
completedAt |
string |
ISO 8601 timestamp. |
duration |
double |
Run duration in milliseconds. |
inputs.* |
map<string,dyn> |
Input values supplied to the run. |
tags.* |
map<string,dyn> |
Tags on the run. |
instanceId |
string |
Instance UUID of the run. |
triggerSource |
string |
What triggered the run. |
failedStep |
string |
Name of the step that failed (if any). |
failureReason |
string |
Failure reason text (if any). |
Missing map keys (e.g. inputs.commit when no commit input was supplied)
return false rather than throwing an error.
# Filter by input value
swamp workflow history search --filter 'inputs.commit == "b3ff3a8a"'
# Filter by status
swamp workflow history search --filter 'status == "failed"'
# Combine with a query and filter by input
swamp workflow history search verify-reviews --filter 'inputs.branch == "main"'
# Filter by duration (runs longer than 60 seconds)
swamp workflow history search --filter 'duration > 60000'See the CEL Expressions reference for the full expression language. The same complexity limits as grant conditions apply: 1024 character maximum, AST depth 24, comprehension nesting 2, cost budget 500.
swamp workflow run search [query]
Search workflow runs with additional filtering options.
| Argument | Required | Description |
|---|---|---|
query |
No | Search query. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--since <duration> |
Only runs started within duration (1h, 1d, 7d, 1w, 1mo). |
--status <status> |
Filter by run status (pending, running, succeeded, failed). |
--workflow <name> |
Filter by workflow name. |
--tag <tag> |
Filter by tag (KEY=VALUE). Repeatable. |
--limit <n> |
Maximum results (default: 50). |
--server |
Target a remote swamp serve instance. |
--token |
Authentication token for the remote server. |
In --json output, two fields mark a run that needs a resume. Both are omitted
when false:
| Field | Type | Description |
|---|---|---|
awaitingResume |
boolean | The run is suspended with every approval gate decided |
workflowHasInputs |
boolean | The run's workflow declares inputs, so a resume may need --input to supply values |
{
"query": "",
"results": [
{
"runId": "34947b42-8b10-4290-b5cb-8284a4f5b452",
"workflowId": "5725fbde-c789-43eb-9eaf-70afb8d10b97",
"workflowName": "gated-deploy",
"status": "suspended",
"startedAt": "2026-09-23T22:17:12.492Z",
"triggerSource": "manual",
"stepProgress": {
"completed": 3,
"total": 4
},
"awaitingResume": true
}
]
}swamp workflow history logs <run_id_or_workflow>
Show logs for a specific workflow run.
| Argument | Required | Description |
|---|---|---|
run_id_or_workflow |
Yes | Run ID or workflow name. |
| Flag | Description |
|---|---|
--repo-dir |
Path to the Swamp repository. |
--tail <n> |
Show only the last N lines. |
--server |
Target a remote swamp serve instance. |
--token |
Authentication token for the remote server. |