Loop Node Execution#
The Loop node is a workflow container that repeatedly executes an internal subgraph until a configurable break condition is satisfied or the maximum iteration count is reached. All loop execution logic lives in api/core/workflow/nodes/loop/loop_node.py.
Key mechanics:
- Each iteration spawns a fresh
GraphEngineinstance for the subgraph. - A shared
variable_pool(ongraph_runtime_state) holds all variables — both loop-scoped and subgraph node outputs — keyed bynode_id. _extract_loop_node_ids_from_config()identifies which node IDs belong to the loop's child graph, used for scoped cleanup.- Break conditions are evaluated at the
LOOP_ENDevent against whichever condition variables are currently present in the pool ("available conditions" approach) . - Loop completion reason is tracked via
LoopCompletedReason(LOOP_BREAKorLOOP_COMPLETED) inapi/core/workflow/nodes/loop/entities.py.
Known Bugs & Fixes#
1. Break condition evaluated before internal nodes finish (Aug 2025)#
Issue #24183 — Loop exit conditions rejected variables produced by nodes inside the loop (e.g., Variable Assigner) because the break check ran before those nodes had executed.
Fix (PR #24257): Moved break-condition evaluation to the LOOP_END event. Also changed from "all condition variables must exist" to an available-conditions approach — only conditions whose variable_selector exists in the pool are evaluated; if none are available, the check defaults to True (preserving prior behavior).
2. Loop variable stored as object instead of raw value (Oct 2025)#
Issue #26862 — Loop variables couldn't increment correctly and break-condition evaluation was unreliable because the variable object (not its value) was written into variable_pool.
Fix (PR #26961): In LoopNode._run(), changed the variable_pool.add(...) call to store variable.value instead of variable, so loop counter/state variables are correctly persisted and can be evaluated or incremented each iteration.
3. Stale subgraph variables leak across iterations (Dec 2025)#
Root cause: variable_pool is a shared object; node outputs written during iteration N (keyed by node_id) persist into iteration N+1. In streaming mode, when a new response coordinator is created for a subsequent iteration with no new chunks, it falls back to the stale value from the prior iteration.
Fix (PR #30059, MERGED): Added _clear_loop_subgraph_variables(loop_node_ids) — called at the start of every iteration before the graph engine is created — which removes all variable-pool entries for subgraph node IDs. This prevents any node output from leaking forward.
A parallel fix (PR #30060, CLOSED) proposed _cleanup_sub_graph_variables_from_previous_iteration() (only running for i > 0); it was superseded by PR #30059's approach of always clearing.
4. Assign Variable node skipped inside loop (v1.14.2–1.15.0)#
Issue (#38246) — Assign Variable nodes placed inside a loop never execute. They also lack the individual test (▶) button in the UI.
Root cause: A frontend bug — the parentId field is not set on Assign Variable nodes when they are added to a loop via the canvas. Without parentId, the backend's loop graph-builder does not recognize them as child nodes and excludes them from the subgraph execution entirely. Code nodes added to the same loop correctly receive parentId, loop_id, and isInLoop: true.
Workaround: Drive break conditions from Code node outputs; move Assign Variable nodes outside the loop to persist final values; or use the loop's loop_variables to carry state between iterations .
5. Single-node run of loop/iteration fails (Jan 2026)#
Issue #28114 — Running a single loop or iteration node in isolation (the "test node" flow) failed due to two problems: (a) an empty SystemVariable was constructed instead of a real one, and (b) the full graph validator rejected an incomplete graph.
Fix (PR #31470, MERGED):
_prepare_single_node_execution()now initializesVariablePoolwithSystemVariable.default()(generates a realworkflow_execution_id).Graph.init()gained askip_validation: bool = Falseparameter; single-node runs passskip_validation=True.
6. Answer node streaming lost inside loop (Jul 2026)#
Issue #38867 — Answer nodes inside a loop (or iteration) execute correctly but produce no SSE message events; the client only sees message_end. Broken since v1.15.0.
Root cause (PR #39208, OPEN): Since graphon 0.5, streaming is synthesized by ResponseStreamFilter sessions. The parent filter can't activate sessions for inner answer nodes (no traversal path from parent root), and child GraphEngine instances built by _WorkflowChildEngineBuilder had no filter at all.
Fix: Added DifyResponseStreamFilter (skips registering nodes unreachable from the graph root; passes through chunks with in_loop_id/in_iteration_id set), and introduced ResponseFilteredChildEngine — each loop pass wraps its child engine in this filter so inner answer nodes stream correctly.
Key Files & Entry Points#
| File | Purpose |
|---|---|
api/core/workflow/nodes/loop/loop_node.py | Core loop runner: _run(), _run_single_loop(), _clear_loop_subgraph_variables(), _extract_loop_node_ids_from_config() |
api/core/workflow/nodes/loop/entities.py | LoopCompletedReason enum (LOOP_BREAK, LOOP_COMPLETED) |
api/core/workflow/enums.py | WorkflowNodeExecutionMetadataKey.COMPLETED_REASON |
api/core/workflow/runtime/variable_pool.py | VariablePool — shared state across iterations; remove([node_id]) used for cleanup |
api/core/workflow/workflow_entry.py | _WorkflowChildEngineBuilder, ResponseFilteredChildEngine ; iter_dify_graph_engine_events() |
api/core/workflow/response_stream_filter.py | DifyResponseStreamFilter — handles streaming for nodes inside loop/iteration containers |
api/core/workflow/graph/graph.py | Graph.init(skip_validation=True) — allows single-node runs |
api/tests/unit_tests/core/workflow/nodes/loop/ | Unit tests for loop node variable cleanup and behavior |