Loops
Some work is iterative by nature. Three rounds of research, each refining the last. A list of items, each handled the same way. A back-and-forth refinement that stops when the output is good enough. A loop node handles all three patterns.
Until a condition is met
The most common loop. Run the body, check a condition, run again if the condition isn't met yet:
nodes:
- id: refine-draft
type: loop
max_iterations: 5
until:
type: prompt
prompt: |
Is this draft ready to ship? Respond yes or no.
$iteration.output
body:
- id: write-draft
type: prompt
prompt: |
Write a draft based on the brief: $BRIEF
{{#previous}}
The previous draft had this feedback: $previous.feedback
Address it directly.
{{/previous}}Z.E.N. runs the body, checks the until: condition, runs the body again if the condition returned "no." The loop stops when until: returns yes or when max_iterations: is reached, whichever comes first.
A fixed number of times
For research workflows, content batches, anything where you know up front how many rounds you want:
- id: research-rounds
type: loop
iterations: 3
body:
- id: search-round
type: prompt
prompt: |
Round $iteration of 3. Find sources we haven't seen yet on "$TOPIC".
Already seen: $previous.sourcesThree rounds. Each round can see what the previous round did via $previous.<field>.
Per item in a list
To run the body once for each thing in a list:
- id: process-each
type: loop
over: $items.output
body:
- id: handle-item
type: prompt
prompt: |
Handle this item: $item
Return the handled version.If $items.output is a list of ten things, the body runs ten times. The current item is $item. Z.E.N. runs them serially by default; add concurrency: 4 on the loop node to fan out.
What $iteration and $previous mean
Inside a loop body, you have two extra variables:
$iterationis the current iteration index (1-based) plus the output of the body so far this round.$previousis the body output from the prior iteration, if there was one.
You use these to write a body that builds on what the prior round did, instead of starting from scratch each time.
When the loop fails
If a body iteration fails, by default the whole loop fails. Override with on_failure: continue to keep going past failures:
- id: process-each
type: loop
over: $items.output
on_failure: continue
body:
- id: handle-item
type: prompt
...When the loop finishes, the loop node's output is the list of body outputs (with null entries for any iteration that failed).
When to use a loop vs. just more nodes
If you know up front you want exactly three rounds and each round is different, write three nodes. If the iteration count depends on something dynamic, or all iterations do the same thing, use a loop. The line is "if I'd be copy-pasting nodes, I should use a loop."