Loop Nodes
DAG workflow nodes support a loop field that runs an AI prompt repeatedly
until a completion condition is met. Each iteration is a full AI agent session
that can read files, write code, run commands, and produce output.
Use loop nodes for autonomous multi-step work: implement N stories from a PRD, iterate on a design until validation passes, or refine output until quality criteria are met.
A loop node’s iteration prompt can live inline (loop.prompt) or in a
command file (loop.command, resolved the same way
command: nodes load their text). Provide
exactly one — both at once, or neither, is rejected at workflow load time.
Quick Start
Section titled “Quick Start”name: iterate-until-donedescription: Implement stories one at a timenodes: - id: setup bash: | echo "Found 3 stories to implement"
- id: implement depends_on: [setup] loop: prompt: | Read the PRD and implement the next unfinished story. Validate your changes before committing.
Setup context: $setup.output User request: $USER_MESSAGE
When all stories are done, output: <promise>COMPLETE</promise> until: COMPLETE max_iterations: 10 fresh_context: true
- id: report depends_on: [implement] prompt: | Summarize what was implemented: $implement.outputHow It Works
Section titled “How It Works”A loop node iterates its prompt until one of these conditions is met:
- LLM completion signal — the AI outputs
<promise>SIGNAL</promise>where SIGNAL matches theuntilvalue - Deterministic bash check — an
until_bashscript exits with code 0 - Structured field — the iteration’s validated
output_formatpayload has theuntil_fieldproperty set totrue - Max iterations reached — the node fails with a clear error
A loop must declare at least one of until / until_bash / until_field — none
is required on its own. They are OR’d: whichever fires first ends the loop, and the
cheap channels are checked before until_bash, which is skipped once another one
fired (it cannot change the outcome, and skipping it avoids an extra run of a
side-effecting script).
Each iteration is a full AI agent invocation with tool access. Between iterations, the executor checks for workflow cancellation.
Choosing a completion channel
Section titled “Choosing a completion channel”| Your completion condition | Declare |
|---|---|
| Externally checkable — tests pass, a file exists, a state field flipped | until_bash only. No prose is matched, so a sentinel the model happens to emit while reasoning about the criteria cannot end the loop early. |
| A judgment the model makes, no human in the loop | output_format + until_field. The decision becomes a schema-validated boolean instead of a string match — the model cannot end the loop by mentioning a word. |
| A judgment the model makes, shown to a human at a gate | until (with interactive). The iteration’s output is a message a person reads and replies to, so prose is the right medium — declaring output_format would replace it with JSON. |
Declaring more than one is fine and means “whichever fires first ends it”.
Configuration Fields
Section titled “Configuration Fields”- id: my-loop loop: prompt: "..." # Inline prompt. Exactly one of `prompt` or `command` is required. # command: <name> # Alternative to `prompt`: package-local or shared command name, # # loaded once per run and reused for every iteration. # # Never combine with `prompt` — the loader rejects both together. until: COMPLETE # Prose completion signal. max_iterations: 10 # Required. Hard limit — node fails if exceeded. fresh_context: true # Optional. Default: false. until_bash: "..." # Bash script checked after each iteration; exit 0 = complete. until_field: done # Property in this node's `output_format` whose validated # value `true` ends the loop. # # At least ONE completion channel is required — any of # until / until_bash / until_field. None is required alone. interactive: true # Optional. Default: false. Pause after each non-completing # iteration for user input via /workflow approve. gate_message: "..." # Required when interactive: true. Message shown to the # user at each pause with the run ID and approve command. signal_completes: true # Optional. Default: false. Interactive loops only: a detected # completion signal completes the node immediately (even on # iteration 1) instead of gating for confirmation.prompt
Section titled “prompt”The prompt text sent to the AI each iteration. Supports all standard variable substitution:
| Variable | Value |
|---|---|
$ARGUMENTS / $USER_MESSAGE | Original user message |
$ARTIFACTS_DIR | Workflow artifacts directory |
$BASE_BRANCH | Repository base branch |
$DOCS_DIR | Documentation directory path (default: docs/) |
$WORKFLOW_ID | Current workflow run ID |
$nodeId.output | Output from upstream nodes |
$LOOP_USER_INPUT | User feedback provided via /workflow approve <id> <text> at an interactive loop gate. Only populated on the first iteration of a resumed interactive loop; empty string on all other iterations. |
$LOOP_PREV_OUTPUT | Cleaned output of the previous loop iteration. Empty string on the first iteration. Useful for fresh_context: true loops that need to reference what the previous pass produced or why it failed. |
$USER_MESSAGE is particularly important for fresh_context: true loops —
the agent has no memory of prior iterations, so the prompt must include all
context needed to continue the work. $LOOP_PREV_OUTPUT complements this by
exposing the previous iteration’s own output without forcing the engine to
thread the session.
command
Section titled “command”Alternative to prompt — names a command file
whose body is loaded as the iteration prompt. Exactly one of prompt or
command is required; specifying both, or neither, is rejected at workflow
load time with a clear error.
The named command resolves exactly like a command: node.
In a packaged workflow it resolves only from that workflow’s commands/
directory, including after include: expansion. In a legacy workflow it uses
repo → home → bundled precedence. The same command-name safety
rules apply — no path separators, no .., no leading . — and unsafe names
are rejected at parse time. Static workflow validation also flags a
loop.command that points at a missing file, with guidance to create it in the
owning workflow’s commands/ directory for packaged workflows or in
.archon/commands/ for legacy workflows, the same way it does for command:
nodes.
For an ordinary workflow, the file is read once per run when the loop node
starts. For a workflow composed through include:, Archon resolves and compiles
the command body during load-time composition so its node references and declared
inputs are proven before the child joins the parent’s flat DAG. A matching file
that is unreadable fails closed; Archon never falls through to a lower-precedence
command with the same name. A missing, empty, unreadable, or non-hermetic included
command fails before a fresh AI turn.
For any interactive loop — whether authored with prompt or command — Archon
persists the resolved prompt template at the gate. A resumed run prefers that
snapshot, so editing inline YAML or a command source while the run is paused does
not change its prompt; deleting a command source does not break that resume either.
Source edits affect fresh runs, which still require successful command resolution
or compilation.
Once loaded, the text behaves identically to an inline prompt: all the
variable substitution above applies unchanged (including $LOOP_PREV_OUTPUT
and $LOOP_USER_INPUT), as do all the iteration semantics (until,
until_bash, max_iterations, fresh_context, interactive /
gate_message).
- id: implement model: opus depends_on: [generate-prd] loop: command: archon-ralph-implement # resolved repo → home → bundled (see precedence above) until: COMPLETE max_iterations: 15 fresh_context: trueUse this when the iteration prompt is long enough that keeping it inline
obscures the shape of the pipeline — Ralph-style implement/build loops and
iterate-until-valid loops are the typical case. It is the loop-node parallel
of moving a prompt: node to a command: node.
The completion signal string. The executor checks each iteration’s output for:
- Tag format (recommended):
<promise>COMPLETE</promise>— case-insensitive match (both tags and signal value), whitespace-tolerant. Prevents false positives from the AI mentioning the signal word in discussion. - Plain signal (fallback): The signal at the very end of output (trailing whitespace and punctuation tolerated) or on its own line. More prone to false positives — prefer the tag format.
The <promise> tags are automatically stripped from output sent to the user
and to downstream nodes.
Optional, like every completion channel. Omit until entirely whenever another
channel decides completion — the executor then never matches prose at all, so the
signal cannot false-positive on a model that mentions its own exit criterion. What
the loader requires is that at least one channel is declared; a loop with none is
rejected at load.
max_iterations
Section titled “max_iterations”Hard safety limit. If the loop reaches this count without meeting any declared completion channel, the node fails (not succeeds). This prevents runaway loops from burning tokens indefinitely.
Choose based on the work scope:
- Simple refinement loops: 3–5
- Multi-story implementation: 10–15
- Long-running autonomous agents: 15–20
fresh_context
Section titled “fresh_context”Controls session continuity between iterations:
| Value | Behavior | Use when |
|---|---|---|
true | Each iteration starts a fresh AI session. No memory of prior iterations. | Work state lives on disk (files, git). Prevents context window exhaustion on long loops. |
false (default) | Sessions thread — each iteration resumes the prior conversation. | Iterative refinement where the agent needs to remember what it tried before. |
The first iteration is always fresh regardless of this setting.
A structured-output re-ask (see until_field) does not disturb
threading. It runs in its own throwaway session so an invalid turn is not carried
forward as context, and that session is discarded — the next iteration still resumes
the conversation the loop was already threading. The repaired answer still reaches it
through $LOOP_PREV_OUTPUT.
until_bash
Section titled “until_bash”Bash script executed after each iteration. If it exits with code 0, the loop
completes — even if the AI didn’t output the completion signal. Optional when
until is set; on its own it is the whole completion channel, which is the
preferred shape whenever the condition is externally checkable:
loop: prompt: "Fix the failing tests" max_iterations: 5 until_bash: "bun run test" # Loop ends when tests pass. No `until:` — nothing to # false-positive on, and no dead field to invent.until_bash runs only on iterations that no other completion channel already
ended, so a loop declaring more than one never pays for a redundant check.
This is useful for deterministic completion criteria: test suites, lint checks,
build success. The bash script supports the same variable substitution as
prompt ($ARTIFACTS_DIR, $nodeId.output, etc.). Note: $nodeId.output
values are shell-escaped when substituted into until_bash. The same
double-quoting footgun that applies to bash: nodes applies here — see
Shell Quoting in bash: vs script:
for the unquoted idiom to use.
until_field
Section titled “until_field”Names a property in this node’s output_format schema.
The loop ends on the first iteration whose validated payload has that property
set to exactly true.
- id: triage output_format: type: object properties: done: { type: boolean } remaining: { type: integer } required: [done] loop: prompt: | Work the next item in the backlog. Report whether the backlog is now empty. max_iterations: 20 until_field: doneUse this when completion is a judgment the model makes and there is nothing on disk to test. It replaces a prose sentinel with a schema-validated boolean, so the model cannot end the loop by mentioning a word while reasoning about its criteria.
Four rules are checked at load time, so a mistake fails the workflow rather than looping silently:
- the node must declare
output_format; - the property must appear in
output_format.properties; - it must be listed in
output_format.required— an optional property the model omits would read as “not complete” and burnmax_iterations; - if it declares a
type, that type must beboolean.
Termination is strict identity: true ends the loop, and nothing else does — not
"true", not 1. A payload that fails schema validation is a failure, not a
quiet “not done yet” (see below).
The loop’s output becomes the validated JSON. With output_format declared,
$loopId.output is the payload rather than the prose, $loopId.output.<field> gets
the same strict field access every other producer enforces, and $LOOP_PREV_OUTPUT
carries the same value.
Per-provider behavior is the same as any other node. On a provider with enforced structured output (Claude, Codex, OpenCode) the schema constrains decoding. On a best-effort provider (Pi, Copilot) the schema is appended to the prompt, and a payload that fails validation is re-asked up to three times within the same iteration — a reask does not consume a loop iteration. If those are exhausted, the node fails with the validation errors; it is never treated as an incomplete iteration. See provider capabilities.
until_field is not supported on loop_group, and deliberately so: a group’s
body node can already declare output_format and be read by a deterministic check,
which is the existing way to express the same thing.
# loop_group equivalent — no new field neededloop_group: max_iterations: 10 until_bash: '[ $decide.output.done = "true" ]' nodes: - id: decide output_format: type: object properties: { done: { type: boolean } } required: [done] prompt: Is the backlog empty?Patterns
Section titled “Patterns”Stateless agent (Ralph pattern)
Section titled “Stateless agent (Ralph pattern)”Each iteration reads state from disk, does one unit of work, writes state back. The prompt tells the agent it has no memory and must bootstrap from files.
- id: implement depends_on: [setup] idle_timeout: 600000 loop: prompt: | You are in a FRESH session — no memory of previous iterations. Read the PRD tracking file to find the next unfinished story. Implement it, validate, commit, update tracking. When all stories are done: <promise>COMPLETE</promise>
Project context: $setup.output until: COMPLETE max_iterations: 15 fresh_context: trueWhen to use: Multi-story implementation, long-running tasks where context
window exhaustion is a risk. The agent reads .archon/ralph/*/prd.json or
similar tracking files to know what’s done and what’s next.
Retry-on-failure with $LOOP_PREV_OUTPUT
Section titled “Retry-on-failure with $LOOP_PREV_OUTPUT”When fresh_context: true is needed (to keep each iteration’s context window
small) but the agent still benefits from knowing what the previous pass said —
typical of implement→validate or generate→review loops — inject the previous
iteration’s output via $LOOP_PREV_OUTPUT:
- id: implement-and-qa loop: prompt: | Implement the plan, then run `bun run validate`. If checks fail, fix the failures.
Previous iteration output (empty on first pass): $LOOP_PREV_OUTPUT
Use the above to focus your fixes. When all checks pass output: <promise>QA_PASS</promise> until: QA_PASS fresh_context: true max_iterations: 3In a continuous run, the first iteration sees $LOOP_PREV_OUTPUT substituted
to an empty string; iterations 2+ see the previous iteration’s cleaned output
(after <promise> tags are stripped).
When a loop resumes from an interactive approval gate, the first executed
iteration after the resume also receives an empty $LOOP_PREV_OUTPUT even if
its numeric iteration is 2+ — the prior output lived in a different run and is
not carried across the gate.
Accumulating context
Section titled “Accumulating context”The agent builds on its own prior work across iterations. Good for iterative refinement where remembering previous attempts matters.
- id: refine loop: prompt: | Review the current implementation and improve it. Run validation after each change. When validation passes with zero issues: <promise>DONE</promise> until: DONE max_iterations: 5 fresh_context: falseWhen to use: Fix-iterate cycles, design refinement, test-driven development where the agent needs to remember what it already tried.
Deterministic exit with until_bash
Section titled “Deterministic exit with until_bash”Combine LLM work with a deterministic completion check:
- id: fix-tests loop: prompt: | Run the test suite. Read the failures. Fix them one at a time. If all tests pass: <promise>TESTS_PASS</promise> until: TESTS_PASS max_iterations: 8 until_bash: "bun run test" fresh_context: falseThe loop ends either when the AI signals completion or when the bash check succeeds — whichever comes first.
No channel vetoes another. They are OR’d, so an AI that emits TESTS_PASS
while tests still fail ends the loop anyway — the bash check never gets to disagree. If the tests
are the real exit criterion, say so by dropping until and the sentence that
teaches the model to emit it:
- id: fix-tests loop: prompt: Run the test suite. Read the failures. Fix them one at a time. max_iterations: 8 until_bash: "bun run test" fresh_context: falseNow the only way out is a passing suite.
Node Features
Section titled “Node Features”What works on loop nodes
Section titled “What works on loop nodes”depends_on— upstream dependencieswhen— conditional executiontrigger_rule— join semanticsidle_timeout— per-iteration timeout (default: 30 minutes)provider/model— node-level overrides are resolved and used for every iteration$nodeId.output— downstream nodes receive the last iteration’s output
interactive and gate_message
Section titled “interactive and gate_message”Set interactive: true to pause the loop between iterations and wait for human input.
After each iteration the executor:
- Sends the gate message to the user along with the run ID and a
/workflow approvecommand. The gate text is engine-generated: a status line naming the completion channel or channels that ended the iteration, or every declared channel when none did, plus a bounded excerpt of the iteration output — followed by yourgate_message, so the gate always reports the real iteration outcome. The status line leads the persisted gate message (metadata.approval.message, also theapproval_requestedevent data — whatworkflow get --jsonandmanage_runread); the chat-delivered message wraps the same text in a⏸ Input required (loop ..., iteration N):prefix, so in chat the status line appears right after that prefix. - Pauses the workflow run
- Waits — the workflow resumes when the user runs
/workflow approve <id> [feedback]
The user’s feedback is injected into the next iteration’s prompt via $LOOP_USER_INPUT.
Approve semantics (finalize vs iterate). What an approve does depends on whether any declared completion channel ended the paused iteration:
- Gate paused on a completed iteration (the status line starts
✅ Completion condition met viafor one completed channel or✅ Completion conditions met viafor several):/workflow approve <id>with no feedback accepts the completion — the node finalizes from the already-computed output and the workflow proceeds, with no re-run. Approving with feedback discards that completion and runs another iteration with your feedback as$LOOP_USER_INPUT. - Gate paused without a completed condition (the status line starts
⚠️ No completion condition met in this iteration): both forms run another iteration (there is nothing to finalize).
The same rule applies on every approve surface: chat /workflow approve, the CLI
(archon workflow approve <id> [--json] — omit the comment to finalize), the HTTP
endpoint (omit comment), the web console (“Accept & complete” with an empty comment
field), and the manage_run chat tool (no message, or accept: true).
A plain chat message at a loop gate is not itself an approve — the same rule now holds
at approval gates. Say what you want and
the agent resolves the gate for you, passing your words through as the feedback; ask a
question and nothing is resolved. Because your words travel as the approve comment, that
route iterates. To finalize, use /workflow approve <id> with no comment, the CLI/web
surfaces above, manage_run with accept: true, or signal_completes.
signal_completes — autonomous completion
Section titled “signal_completes — autonomous completion”By default an interactive loop always gates first, even when the very first iteration reaches
a completion condition — the human confirms before the node completes. If you want a completed
iteration to finish the node immediately (no gate, no approve), set signal_completes: true:
- id: validate loop: prompt: | Run the validation suite. On PASS output <promise>VALIDATED</promise>. On failure, describe what failed and wait for instructions. until: VALIDATED max_iterations: 5 interactive: true gate_message: Validation did not pass — review the failures above. signal_completes: true # signal ⇒ node completes immediately, no gateWith signal_completes: true the gate only appears on iterations where no declared completion
channel fired — the pattern for “pass through on success, pause for a human on failure”. The flag
has no effect on non-interactive loops (a completed iteration already finishes them); setting it
without interactive: true emits a loader warning.
AI approvers / relay steering. An orchestrating agent can steer another run’s gate:
read the structured gate state first (archon workflow get <id> --json →
.metadata.approval.completionSignaled, or the manage_run get action, which prints
completionSignaled, the iteration, and an output excerpt), then finalize with
archon workflow approve <id> --json (no comment) or manage_run approve with
accept: true — or iterate by passing feedback. The --json approve records the decision
without resuming; a later resume executes the finalize or the next iteration.
Note: Interactive loop nodes require
interactive: trueat the workflow level as well. If only the loop node hasinteractive: true, a loader warning is emitted and the workflow will not pause correctly in web background mode.
name: guided-refinedescription: Refine output with human review between iterations.interactive: true # Required at workflow level for interactive loopsnodes: - id: refine loop: prompt: | Review the current draft and improve it based on this feedback: $LOOP_USER_INPUT
When the output is satisfactory, output: <promise>DONE</promise> until: DONE max_iterations: 5 interactive: true gate_message: Review the output above. Reply with your feedback or type DONE to finish.What is NOT supported on loop nodes
Section titled “What is NOT supported on loop nodes”retry— rejected at parse time. The loader fails the workflow ifretry:is set on a loop node.context: fresh— silently ignored. Session control is handled exclusively byfresh_contextwithin theloop:confighooks— per-node SDK hooks are not passed through to loop iterationsmcp— per-node MCP server configs are not loaded for loop nodesskills— skill preloading is not applied to loop iterationsallowed_tools/denied_tools— tool restrictions are not enforced on loop iterations
These fields (except retry) are silently discarded at parse time with a
loader warning — the workflow still loads but the fields have no effect.
retry is the exception: it causes a hard load error.
The loop executor manages its own AI sessions independently from the standard node executor. If you need hooks, MCP, skills, or tool restrictions, consider using a command node that wraps the iterative logic in a command file.
Output
Section titled “Output”A loop node’s output (available via $nodeId.output to downstream nodes) is
the last iteration’s output only — not a concatenation of all iterations.
If you need to accumulate results across iterations, write them to files in
$ARTIFACTS_DIR and have the downstream node read from there.
Error Handling
Section titled “Error Handling”| Scenario | Behavior |
|---|---|
| Iteration throws an error | Node fails immediately (no more iterations) |
| Max iterations exceeded | Node fails with descriptive error |
| Workflow cancelled | Detected between iterations, node stops |
| Idle timeout per iteration | Iteration completes with whatever output was collected; loop continues to next iteration |
retry configured on node | Rejected at parse time — workflow fails to load |
Cross-Node Loops with loop_group
Section titled “Cross-Node Loops with loop_group”A loop node iterates a single prompt. A loop_group node iterates a
multi-node sub-DAG — a sealed body of nodes that re-runs in full each
iteration until a completion condition is met. Use it when “iterate until done”
needs a pipeline of steps (e.g. implement → test → review), not just one
repeated step.
name: fix-until-greendescription: Implement, test, and review until tests passnodes: - id: fix-loop loop_group: until: TESTS_PASS # completion signal in the body's terminal output max_iterations: 5 fresh_context: false # false (default) = body AI sessions continue across iterations nodes: # sealed sub-DAG body — repeats as a unit - id: implement prompt: Fix the failing tests. Emit TESTS_PASS only when all pass. depends_on: [] - id: test bash: | if bun test; then printf 'green' else printf 'red' fi depends_on: [implement] - id: review prompt: Summarize the result; echo TESTS_PASS if tests are green. depends_on: [test]How it works
Section titled “How it works”- From the outer DAG’s perspective,
fix-loopis one node. The cycle is encapsulated inside it — the outer DAG stays acyclic. - Each iteration runs the body’s topological layers in full (concurrent nodes within a layer run in parallel, same as a normal DAG).
- The body is sealed: a body node’s
depends_onmay only reference sibling body nodes, not outer-DAG nodes. Outer context is still reachable via$nodeId.outputrefs in body prompts. - Loop-control and body lifecycle events use the namespaced step name
{groupId}.{nodeId}in the persisted event log, with the iteration recorded in event data. Nested groups compose the prefix. - A body node that fails fails the whole group immediately with that node’s error (no further iterations run) — same semantics as a failed node in a top-level DAG.
$fix-loop.output(visible to the outer DAG) is the final iteration’s terminal-node output.
Reusing a block in the body
Section titled “Reusing a block in the body”include: is valid in a loop_group body. Discovery expands the block before the run starts,
so each iteration executes ordinary namespaced body nodes — there is no child run or
runtime-resolved graph:
- id: converge loop_group: until_bash: test $review.output.ready = "true" max_iterations: 5 nodes: - id: review include: reusable-review with: request: $INPUTS.requestThe block’s returns: node supplies $review.output, including in until_bash. Internal
dependencies, inputs, gates, and namespacing behave the same as for a top-level include. A
workflow: node still cannot appear in a group body because it creates a separately governed
runtime sub-run rather than static composition.
Cross-iteration references: $LOOP_PREV
Section titled “Cross-iteration references: $LOOP_PREV”A body node can reference a sibling’s output from the previous iteration
with $LOOP_PREV.<nodeId>.output (and $LOOP_PREV.<nodeId>.output.<field>
for structured output):
nodes: - id: fix-loop loop_group: # The terminal node's own verdict ends the loop; nothing here emits a # prose sentinel, so `until:` would run to max_iterations on a green run. until_bash: test $test.output = "green" max_iterations: 5 nodes: - id: implement prompt: | Previous attempt's test output: $LOOP_PREV.test.output Fix what failed. depends_on: [] - id: test bash: | if bun test; then printf 'green' else printf 'red' fi depends_on: [implement]On iteration 1 (no prior iteration), $LOOP_PREV.* resolves to an empty
string. Field access uses the same strict semantics as $nodeId.output.field
(a field not in the producer’s declared schema fails the consuming node rather
than silently degrading).
A body node can also gate on the previous iteration’s typed output with when::
- id: repair prompt: Repair the failing tests. depends_on: [test] when: "$LOOP_PREV.test.output == 'red'"This is a condition reference, not text substitution. On iteration 1 it resolves to
'', so the non-empty equality is false. See Condition Syntax.
$LOOP_PREV works in a body node’s with: binding string values too, with the
same text semantics as every other body surface: the previous iteration’s output is
substituted into the value each pass (so with: { prev: $LOOP_PREV.test.output }
delivers last iteration’s text, '' on iteration 1 — never a typed value). It is
not a node reference, so a binding directive’s from: cannot read it — the
loader rejects that at load and names the string form instead.
Configuration fields
Section titled “Configuration fields”loop_group shares the same iteration-control fields as loop:
until, max_iterations,
fresh_context, until_bash,
interactive, and gate_message — including the
at-least-one-of until / until_bash rule and the short-circuit between them. The
difference is the body: loop takes a single prompt; loop_group takes a
nodes array.
As with a loop: node (whose node-level model/provider are resolved and
applied to every iteration), model and provider set on a loop_group node
are honored: they become the default for every body AI node, overridable per
body node.
Resume
Section titled “Resume”Two distinct cases:
- Interactive-gate resume (
/workflow approve <id>): the loop continues with the next iteration’s whole body. Withfresh_context: false, the body’s AI session continues from where it paused (the session cursor is persisted across the gate).$LOOP_PREV.*refs on the first resumed iteration resolve to the prior iteration’s real body outputs, restored from the persisted run history — the same as an un-paused next iteration would have seen. - Failure resume (
/workflow resume <id>after a crash/failure): there is no persisted iteration cursor — the loop_group node restarts from iteration 1. Per-body-node resume granularity is not supported in v1.
What is NOT supported on loop_group nodes (v1)
Section titled “What is NOT supported on loop_group nodes (v1)”until_field/output_format— the group never calls the provider itself, so it has no payload of its own to read. Express the same thing with a body node that declaresoutput_formatplusuntil_bashreading$node.output.field— seeuntil_field.retry(the loop manages its own iteration) — rejected at parse time.persist_sessionfor body AI nodes across iterations — body sessions reset per iteration (governed byfresh_context).- Per-body-node resume (skip-to-failed-body-node) — the whole iteration re-runs.
$LOOP_PREV.<id>.output[N]history indexing — only the immediately prior iteration is reachable.
Nested loop_group inside a loop_group body is supported by construction
(the body is a normal nodes array), but is not hardened in v1.
See Also
Section titled “See Also”- Authoring Workflows — full workflow reference
- Per-Node Hooks — SDK hooks for command/prompt nodes
- Per-Node MCP Servers — external tool integration
- Per-Node Skills — skill preloading