Loops
A step can repeat. There are three ways, and a step uses exactly one:
| Key | Repeats | Bound |
|---|---|---|
foreach: <path or list> |
once per item, optionally parallel: N at a time |
the list’s length (max_iterations caps it) |
until: [guards] |
until every guard holds, checked after each iteration | max_iterations — required |
while: [guards] |
as long as every guard holds, checked after each iteration (do…while) | max_iterations — required |
The repeated thing is either the step itself (it keeps its prompt or run) or a steps: body
that runs in order once per iteration. There is no unbounded loop: until/while without
max_iterations is refused when the file loads, and the hard ceiling is 999 iterations.
foreach
Section titled “foreach”input_schema: type: object properties: files: { type: array, items: { type: string } }
steps: - id: review foreach: input.files # or list.items (an earlier step's array), or [a, b, c] parallel: 3 # optional; 0/1 = one at a time, in order prompt: "Review {{ .Item }} (file {{ .Index }})" tools: [read, grep] output_schema: type: object properties: { verdict: { type: string } }
- id: summary run: echo "{{ .Steps.review.count }} files reviewed"Inside the loop, {{ .Item }} is the current element and {{ .Index }} its 0-based position.
.Item can be an object: {{ .Item.name }}.
A foreach path is input.<key> or <step>.<field>. It is checked at load time: the input or the
step must exist, the field must be declared as an array in its schema, and the step must run
before the loop — in a DAG that means the loop needs it.
A template ("{{ .Input.files }}") is refused; foreach takes a plain path.
until and while
Section titled “until and while”steps: - id: baseline run: go test ./... continue-on-error: true
- id: repair when: [{ path: baseline.ok, equals: false }] until: - path: check.ok equals: true max_iterations: 4 steps: - id: fix prompt: | The suite is red. Last output: {{ with .Steps.check }}{{ .stdout }}{{ else }}{{ .Steps.baseline.stdout }}{{ end }} Fix the first failure. tools: [bash, read, edit] output_schema: { type: object, properties: { cause: { type: string } } } - id: check run: go test ./...Guards are the same shape as when: a path and equals or exists. A loop guard must read
something the loop produces — a body step (check.ok) or, for a step that repeats itself, its own
id (poll.exitCode) — otherwise nothing could change between iterations, and the file is refused.
The next iteration sees the previous one’s outputs: on round 2, {{ .Steps.check.stdout }} is
round 1’s check. A step whose result a loop guard reads is routing on it, so a red check makes
the loop go round again instead of failing the run.
What a loop produces
Section titled “What a loop produces”The loop step’s output is an aggregate, so a later step reads it like any other output:
| Field | Value |
|---|---|
iterations |
an array of every iteration’s output, in item order (even with parallel) |
count |
how many iterations ran |
last |
the final iteration’s output |
For a step that repeats itself an iteration’s output is that step’s output. For a body it is an
object keyed by body step id: {{ .Steps.repair.last.check.ok }}. A body step’s id is local to
the loop: {{ .Steps.fix }} from outside the loop is reported by the dry run as a step that
does not exist.
Every iteration is a row on the run
Section titled “Every iteration is a row on the run”Each iteration is persisted as its own step row — review[0], review[1], …, or
repair[0].fix, repair[0].check, … for a body — with its prompt, output, status, timing and
cost. The run page lists them under their loop step. Because each one is an ordinary step:
- Resume replays the iterations that finished and continues with the first that did not.
- Retry of an iteration (
wfx retry, the UI) resets it and every later one; retrying the loop step resets all of its iterations. A finished loop earlier in the run keeps its record. - Human gates work inside loops.
requires_approvalon a step that repeats itself is asked per iteration (deploy[0], thendeploy[1]); on a loop with asteps:body it is asked once, before the loop; on a body step it is asked for that step in every iteration.approversandprevent_self_approvalare enforced on the iteration exactly as on the step. Gates,ask_humanandneeds_inputbehave as they do anywhere else. - A body step’s own
whenis read against its iteration, so a step can run for one item and be skipped for the next.
The dry run shows the plan
Section titled “The dry run shows the plan”wfx dryrun reports each loop’s mode, the iterations it would create
(review[0] … review[2] for a three-item input; repair[0].fix … repair[3].check for
max_iterations: 4), and checks every body step’s templates as the first iteration would render
them, with .Item and .Index filled in. A step that reads .Item without being a loop is
flagged.
Limits
Section titled “Limits”- No loop inside a loop: split it into two steps.
- A body’s steps run in order;
needs,consumes,producesandskip_toare refused inside one. parallelapplies only toforeach: anuntiliteration depends on the one before it.- At most 999 iterations and 99 body steps per loop.
A complete example is the fix-until-green template.