Skip to main content

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.yaml

The 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-report

id

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 AM
trigger:
  schedule: "*/15 * * * *" # Every 15 minutes
trigger:
  schedule: "0 3,12 * * *" # 3:00 AM and 12:00 PM daily

trigger.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: production

inputs

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:
    - environments

Input 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 search filters 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 command field of a command/shell result)

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_KEY
inputs:
  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: 4

target

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: succeeded

Both 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-report

Required 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 run as 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 vault

Dynamic 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_SECRET

The 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: 0

name

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: succeeded

See 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: false

name

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: succeeded

See 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 == 0

A 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.* or steps.*.
  • 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 modelIdOrName and modelType present
  • modelType without modelName
  • Neither modelIdOrName nor modelType present

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: 3

The 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 the approve grant 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 approvals lists the child's gate and names the parent run.
  • In --json output, the suspended run's approvalRequired names the gate to decide (workflowName, runId, stepId), and waitingOnNestedRun names 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: 24

Each 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: succeeded

Composite 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: skipped

or — any child condition must be true. Requires at least 2 conditions.

condition:
  type: or
  conditions:
    - type: succeeded
    - type: failed

not — inverts a single condition.

condition:
  type: not
  condition:
    type: failed

Composite 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:
      - environment

CEL 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.

  1. 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.
  2. Weight — within the same level, lower weights execute first.
  3. 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-pipeline

The 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 deploy

With --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-fc0247c1b51d

With --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-42

Resume 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 5m

Retry 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-1d2e3f4a5b6c

When 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 forEach iteration, 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:

  1. A workflow run fails at step 3 of 5.
  2. You fix the underlying problem.
  3. You resume with --from targeting step 3.
  4. Steps 1 and 2 are not re-run (already completed).
  5. Step 3 re-runs because its guard sees no completion data for the failed attempt.
  6. 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 3f8a2b1c

Concurrency Resolution Order

When steps within a job execute in parallel, the effective concurrency limit is resolved from the most specific level:

  1. Step concurrency — if multiple steps at the same dependency level specify concurrency, the minimum value across those steps is used.
  2. Job concurrency
  3. Workflow concurrency
  4. 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=4

Validation

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: PASSED

Omit 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: PASSED

Validation rules:

  • id must be a valid UUID v4.
  • name must be at least 1 character, unique within the repository, with no path traversal sequences.
  • version must be a positive integer.
  • concurrency must be a non-negative integer when present (at workflow, job, or step level).
  • tags values must be strings.
  • trigger.schedule must be a valid cron expression.
  • jobs must 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 definition globalArguments must reference inputs declared in the workflow's inputs block.
  • 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.
  • affinity is valid at the workflow and job level. A validation warning is emitted when affinity: true is set but no placement field (target, labels, or platform) 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-report

History

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.