Skip to main content
← Back to list
01Issue
FeatureShippedSwamp CLI
Assigneesstack72

Relationships

#1437 Add guard field to workflow steps for idempotent execution

Opened by stack72 · 7/27/2026· Shipped 7/28/2026

Summary

Add a guard field to workflow steps — a CEL expression evaluated before step execution. When the guard evaluates truthy, the step is skipped (already done). When falsy or absent, the step executes normally.

This is the workflow-level primitive for idempotent step execution, enabling safe workflow resume, re-run, and cron scheduling without re-executing completed steps.

Motivation

Filed as part of the design for #1430 (writeResource commits per-call). The investigation found that protecting against partial data consumption belongs in the workflow layer, not the data layer. Guard gives workflow authors a composable way to express "this step is done" using domain-specific checks.

Industry precedent: Terraform/Pulumi preserve partial state and let the next run diff against reality. Chef/Puppet use idempotent convergence. Guard brings this pattern to swamp workflows.

Design

The guard field

A CEL expression on a workflow step. Truthy means skip, falsy means execute:

steps:
  - name: dispense-reagent
    guard: >
      ${{ data.latest("liquid-handler", "dispense-log").attributes.batchId
          == inputs.batchId }}
    task:
      modelName: liquid-handler
      method: dispense

  - name: calibrate
    guard: >
      ${{ data.latest("plate-reader", "calibration").attributes.status
          == "passed" }}
    task:
      modelName: plate-reader
      method: calibrate

  - name: read-plate
    depends: [dispense-reagent, calibrate]
    guard: ${{ data.latest("plate-reader", "scan-complete").attributes.id }}
    task:
      modelName: plate-reader
      method: read-all

Three patterns, same field:

  • Data truthiness${{ data.latest("plate-reader", "scan-complete").attributes.id }} — truthy scalar means done, null means not done
  • Value comparison${{ data.latest("liquid-handler", "dispense-log").attributes.batchId == inputs.batchId }} — same batch means skip, different batch means re-run
  • Method call (future) — ${{ model.method("infra", "instance-exists", { name: inputs.instanceName }) }} — check external state (deferred to a follow-up, requires CelModelNamespace)

Guard evaluation

  • Evaluated after dependency checks pass, before stepRun.start()
  • Insertion point in execution_service.ts: after shouldStepRun() (line ~2608), before stepRun.start() (line ~2612)
  • Guard expression is evaluated via the existing async CEL evaluation path (celEvaluator.evaluateAsync())
  • The expression context includes inputs, data, self (with forEach variable if applicable)

Events and rendering

  • Add a reason field to the existing step_skipped event: "guarded" vs existing skip (dependency not met)
  • Renderer shows: – step dispense-reagent (guarded) to distinguish from dependency skips

forEach compatibility

Guard expressions can reference self.* (the forEach variable), so each expanded iteration evaluates its own guard independently:

- name: read-plate
  forEach:
    item: well
    in: ${{ range(1, 97) }}
  guard: ${{ data.latest("plate-reader", self.well) }}
  task:
    modelName: plate-reader
    method: read-single-well

On resume, each iteration's guard evaluates independently — completed wells are skipped, failed/unstarted wells execute.

Backwards compatibility

  • guard is optional — steps without it always execute (current behavior)
  • No changes to existing step execution, dependency resolution, or event handling
  • The field is added to StepObjectSchema and automatically recognized by rejectUnknownKeys

Implementation scope

  • src/domain/workflows/step.ts — add guard field to schema, interface, class (DONE in worktree)
  • src/domain/workflows/execution_service.ts — evaluate guard before step execution
  • src/domain/workflows/execution_events.ts — add reason to step_skipped
  • src/presentation/renderers/workflow_run.ts — render guarded skip reason
  • src/domain/workflows/validation_service.ts — CEL syntax validation for guard expressions
  • design/workflow.md — document guard semantics
  • Tests for all of the above
  • #1430 — the parent issue (writeResource atomicity). Guard is one of three features in the design: guard (this issue), rollbackOnFailure (method-level), and resume --from (workflow DAG re-entry).
  • The model.method() CEL function for external-state guards is deferred to a separate issue.
  • workflow resume --from <step> is deferred to a separate issue.
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 4 MOREPR_MERGED+ 1 MORENOTIFICATION_SKIPPED

Shipped

7/28/2026, 12:30:13 AM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack727/27/2026, 10:03:17 PM

Sign in to post a ripple.