Loops
duckflux provides a first-class loop construct for repeating steps — either until a CEL condition becomes true, or a fixed number of times. Both modes can be combined for safety caps.
The loop construct
Section titled “The loop construct”A loop block wraps a sequence of steps in the flow and controls how many times they repeat:
flow: - loop: until: reviewer.output.approved == true max: 5 steps: - coder - reviewerFields
Section titled “Fields”| Field | Type | Required | Description |
|---|---|---|---|
steps | list | Yes | Steps to repeat on each iteration. |
until | CEL expression | One of until or max | Exit condition. Loop stops when this evaluates to true. |
max | integer | One of until or max | Maximum number of iterations. Loop stops after exactly N runs. |
as | string | No | Renames the loop context variable (default: loop). |
Conditional loop — until
Section titled “Conditional loop — until”Repeats a set of steps until a CEL condition evaluates to true:
flow: - loop: until: reviewer.output.approved == true steps: - coder - reviewerThe condition is evaluated after each iteration. If it is true on the first check, the loop exits immediately and does not run again.
Fixed loop — max
Section titled “Fixed loop — max”Runs a set of steps exactly N times, with no exit condition:
flow: - loop: max: 3 steps: - stepAThis is equivalent to writing stepA three times in the flow, but more concise and without the repetition.
Combining until and max
Section titled “Combining until and max”Use both fields together to exit early when a condition is met, while still capping the total number of iterations as a safety measure:
flow: - loop: until: reviewer.output.approved == true max: 5 steps: - coder - reviewerThe loop exits as soon as either condition is satisfied — whichever comes first:
reviewer.output.approvedbecomestrue, or- 5 iterations are completed.
This is the recommended pattern for agentic review cycles, retry loops, and polling workflows.
Loop context variables
Section titled “Loop context variables”Inside a loop block, the following variables are available under the loop namespace:
| Variable | Type | Description |
|---|---|---|
loop.index | int | 0-based iteration index. 0 on the first iteration. |
loop.iteration | int | 1-based iteration number. 1 on the first iteration. |
loop.first | bool | true only on the first iteration. |
loop.last | bool | true on the last iteration. Only set when max is defined. |
Use these in CEL expressions within the loop’s steps:
flow: - loop: max: 5 steps: - coder - reviewer: when: loop.index > 0Here, reviewer is skipped on the first iteration (loop.index == 0) and runs on all subsequent ones.
Renaming the context with as
Section titled “Renaming the context with as”By default the loop context is accessed via loop.*. Use the as field to give the context a semantic name — especially useful when loops are nested or the variable name should reflect the domain:
flow: - loop: as: attempt max: 3 steps: - coder - reviewer: when: attempt.index > 0With as: attempt, the variables become attempt.index, attempt.iteration, attempt.first, and attempt.last instead of the default loop.*.
flow: - loop: max: 3 steps: - coder - reviewer: when: loop.index > 0flow: - loop: as: attempt max: 3 steps: - coder - reviewer: when: attempt.index > 0Both are functionally identical. The as field is purely for readability.
Using when inside loops
Section titled “Using when inside loops”The when guard condition works inside loops to control which steps execute on each iteration:
flow: - loop: as: round until: reviewer.output.approved == true max: 5 steps: - reviewer - coder: when: reviewer.output.approved == falseIn this example:
reviewerruns on every iteration.coderonly runs whenreviewer.output.approvedisfalse— it is skipped on the iteration where approval is granted.
This pattern avoids doing unnecessary work on the final iteration of a conditional loop.
Complete example
Section titled “Complete example”A code review pipeline that combines until, max, as, and when to drive an agentic review cycle:
id: code-review-loopname: Code Review Loopversion: "1"
defaults: timeout: 10m
inputs: branch: type: string default: "main" max_rounds: type: integer default: 5 description: Safety cap for review iterations.
participants: coder: type: exec run: echo '{"status":"coded","diff":"..."}' onError: retry retry: max: 2 backoff: 2s
reviewer: type: exec run: echo '{"approved":true,"score":9,"feedback":""}'
notifyApproved: type: http url: https://hooks.example.com/approved method: POST onError: skip
notifyRejected: type: http url: https://hooks.example.com/rejected method: POST onError: skip
flow: - coder
- loop: as: round until: reviewer.output.approved == true max: workflow.inputs.max_rounds steps: - reviewer - coder: when: reviewer.output.approved == false
- if: condition: reviewer.output.approved == true then: - notifyApproved else: - notifyRejected
output: approved: reviewer.output.approved score: reviewer.output.score rounds: round.iterationWalk-through:
coderruns once before the loop to produce an initial draft.- The loop runs up to
workflow.inputs.max_roundstimes — or exits early oncereviewer.output.approvedistrue. - On each iteration,
revieweralways runs;coderonly runs if the reviewer rejected the output. - After the loop, the
ifblock sends the appropriate notification. - The workflow outputs the final approval status, score, and total number of rounds completed.