coleam00/Archon · warning
loop_group_gate_completion_not_referenced
loop_group_gate_completion_not_referenced
Error message
Node '${gate.id}': this gate is the loop_group's terminal sink, but 'loop_group.until_bash' does not reference '${gateRef}' — the human's decision is captured but never consulted for completion, so the loop only ends via max_iterations. Declare 'until_bash' checking '${gateRef}.decision' (e.g. '[ "${gateRef}.decision" = "approve" ]') so the gate's answer actually drives completion (#2707 step 3). What it means
This loader warning fires when a gate node correctly is a loop_group body's sole terminal sink, but the group's loop_group.until_bash expression never references that gate's output ($<gateId>.output). The human's decision is captured (a node_completed event is written), but the completion check never consults it, so the loop only terminates via max_iterations instead of the human's answer (#2707 step 3). Detection is purely structural: the until_bash string is scanned for output references naming the gate id.
Source
Thrown at packages/workflows/src/loader.ts:560
// string reference the gate's node id), not prose-sniffing — no judgment
// about what the check DOES with it, only whether it looks at it at all.
const untilBash = node.loop_group.until_bash;
const untilBashRefsGate =
untilBash !== undefined &&
Array.from(untilBash.matchAll(new RegExp(OUTPUT_REF_SOURCE, 'g'))).some(
m => m[1] === gate.id
);
if (!untilBashRefsGate) {
const gateRef = `$${gate.id}.output`;
const message =
`Node '${gate.id}': this gate is the loop_group's terminal sink, but ` +
`'loop_group.until_bash' does not reference '${gateRef}' — the human's ` +
'decision is captured but never consulted for completion, so the loop only ' +
`ends via max_iterations. Declare 'until_bash' checking '${gateRef}.decision' ` +
`(e.g. '[ "${gateRef}.decision" = "approve" ]') so the gate's answer actually ` +
'drives completion (#2707 step 3).';
warnings.push(message);
getLog().warn(
{ id: gate.id, warning: message },
'loop_group_gate_completion_not_referenced'
);
}
}
}
// A body-terminal sink that is itself an interactive loop/loop_group can pause
// without stopping THIS group (#2753) — a bare gate sink is excluded here since
// that case is already correctly handled above (and by #2707 step 3 at runtime);
// this covers a sink whose own pause is trapped one level down instead.
if (bodySinks.length === 1) {
const sink = bodySinks[0];
if (!isIncludeDirective(sink) && !isGateNode(sink) && isUnescalatableInteractiveSink(sink)) {
const message =
`Node '${id}': this loop_group's terminal sink ('${sink.id}') is itself an ` +
'interactive loop/loop_group — a pause inside it does not escalate to stop ' +
"this loop_group's own iteration (#2753). The outer loop can run further iterations " +View on GitHub (pinned to 0773b97458)
Solutions
- Add or extend loop_group.until_bash so it reads the gate's output, e.g. until_bash: '[ "$approve.output.decision" = "approve" ]'
- Fix the node id or output path in until_bash so the reference exactly matches the gate node's id
- If the loop is genuinely max_iterations-driven, remove the gate or document that its answer does not drive completion
Example fix
# before
loop_group:
until_bash: '[ "$work.output.done" = "true" ]'
nodes:
- id: work
agent: ...
- id: approve
gate: {}
# after
loop_group:
until_bash: '[ "$approve.output.decision" = "approve" ]'
nodes:
- id: work
agent: ...
- id: approve
gate: {} Defensive patterns
Strategy: validation
Validate before calling
function gateReferencedInUntilBash(group) {
const dependedOn = new Set(group.nodes.flatMap(n => n.depends_on ?? []));
const sinks = group.nodes.filter(n => !dependedOn.has(n.id));
const gate = group.nodes.find(n => 'gate' in n);
if (!gate || sinks.length !== 1 || sinks[0].id !== gate.id) return true; // other warning
return group.until_bash !== undefined && group.until_bash.includes(`$${gate.id}.output`);
} Prevention
- Whenever a loop_group body ends in a gate, immediately write until_bash referencing $<gateId>.output.decision
- Copy the loader's suggested example form: '[ "$<gateId>.output.decision" = "approve" ]'
- Check node ids character-for-character — the structural check is an exact id match
- Test that a rejecting decision actually ends the loop before relying on it in production
When it happens
Trigger: Loading a workflow YAML with a loop_group whose body ends in a valid gate sink while loop_group.until_bash is undefined, references other nodes' outputs only, or references the gate with a misspelled/incorrectly-scoped id so the OUTPUT_REF_SOURCE regex match finds no id equal to gate.id.
Common situations: Authors declaring until_bash against a work node's output while forgetting the gate; typos in $gate-id.output.decision; copying an until_bash template from a non-gate loop; assuming the gate itself ends the loop without wiring the completion check.
Related errors
- loop_group_gate_not_terminal_sink
- loop_group_nested_pause_not_escalated
- node_loop_group_until_deprecated
- Cannot execute run '${detachedPreCreatedRun.id}': it belongs
- Dry-run failed; missing stubs: ${blockingMissingStubs.join('
AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01).
Data as JSON: /api/errors/f2ae7222cae474be.
Report an issue: GitHub.