Loop blocks (until)
Warden sagas support a single-level loop block: a bounded do-while over nested reason / commit steps. After each successful body pass, the engine evaluates until.cel. Loops are always bounded by required max_iterations.
Authoring
steps:
- id: refine
kind: loop
max_iterations: 5
until:
cel: "steps.validate.facts.ok == true"
steps:
- id: attempt
kind: reason
# ...
- id: validate
kind: commit
# ...
- id: finalize
kind: commit
# ...
Rules:
max_iterationsis required and must be>= 1.until.celis required and compile-checked at deploy.- Loop bodies may contain
reason/commitonly (no nested loops in v1). - Multiple sibling loops in one saga are allowed; each has an isolated
context.loops.<id>bucket. - Step ids must be unique across the whole blueprint, including every loop body.
when.celinside a body is allowed;SKIPPEDcounts as a clean step completion and clears that step’s latest-winscontext.steps.<id>entry for the current body pass (sountil.celcannot see a prior iteration’s output).
Runtime model
- Delay-tail materialization: saga start creates only
[prefix] → [first loop iteration 1 body]. Steps after the active loop are minted whenuntil.celbecomes true (or when entering the next sibling loop). forward_seq: every forward row gets a monotonic execution sequence. Scheduling walksforward_seqASC; compensation walksforward_seqDESC.- Latest-wins context:
context.steps.<id>reflects the latest iteration’s outcome (including an empty shell afterSKIPPED). History lives onSagaStepInstancerows (loop_id,iteration,forward_seq). - Hard fail: any body step failure or policy/HITL reject aborts the saga (no further iterations).
- Exhaustion: if
untilstays false aftermax_iterations, the saga fails withLOOP_EXHAUSTEDand compensates.
Compensation
Undo resolves compensation with paths against the forward row being compensated (output_payload / resolved_arguments), not live context.steps, so iteration N cannot bleed into iteration 1's undo.
CEL bindings
until.cel (and when.cel) see input, steps, loops, saga, plus loop: { id, iteration, max_iterations } when evaluating inside a loop.
Smoke path: max_iterations: 1 with until.cel: "true" exits after one body pass and materializes the tail.