Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions docs/src/content/docs/contributing/loop-nodes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
title: Loop Nodes Architecture
---

This page records the implementation contract for the collection-based `For` and `ForReturn` nodes. The durable
contract is shared by the backend graph executor, saved workflow format, and workflow editor.

## Core contract

`For` is a bounded collection loop, not a general `While` node. Its source is one `collection: list[Any]` input. Each
iteration exposes `item`, `index`, `total`, and `state`; the final execution surface exposes `output_collection` and
`final_state`. `ForReturn` closes one iteration and may provide an output item, updated state, and a
`continue_condition`.

Loop state is explicit `LoopState` graph data. It is copied and serialized with normal invocation inputs and results;
it is not stored in transient process-local context. If a return omits state, the previous state carries forward. A
missing or `None` continue condition continues; `False` finalizes the current loop after recording its output and state.

The loop is sequential. A body failure or cancellation stops later iterations and does not release partial final-scoped
outputs. An empty collection is successful: no body node runs, `output_collection` is empty, and `final_state` is the
provided initial state or an empty state.

## Durable loop linkage

Every `For` and `ForReturn` pair is associated by a serialized, direct `loop_linkage` edge:

```text
For.loop_linkage - - - - - - - - - - - - - - - - - > ForReturn.loop_linkage
For.item -> body path -> ForReturn.output
```

This edge is an association, not executable data flow. It is excluded from ordinary input propagation, cycle detection,
and scheduling. The backend requires exactly one outgoing linkage for every `For`, exactly one incoming linkage for every
`ForReturn`, and the exact `For.loop_linkage` to `ForReturn.loop_linkage` endpoints. Default edges using the reserved
`loop_linkage` field are invalid.

The editor may represent the association temporarily as a one-to-one connector alias:

```text
For.loop_linkage -> connector.in -> connector.out -> ForReturn.loop_linkage
```

Every connector on that path must have exactly one input and one output. The path cannot branch, be reused as ordinary
data flow, or terminate at a different node. Graph construction canonicalizes a complete alias to one direct runtime
`loop_linkage` edge. No loop identity or body metadata is inferred or migrated.

## Body and output scopes

Iteration-scoped outputs (`item`, `index`, `total`, and `state`) define the loop body. Final-scoped outputs
(`output_collection` and `final_state`) are available only after the matching loop context completes. Body nodes must
terminate at the linked `ForReturn`; they cannot escape directly to after-loop nodes. Final outputs cannot feed back into
the loop body.

`ForReturn.output` and `ForReturn.state` are scheduler-facing result fields and are hidden as downstream editor outputs.
They are still retained in execution results for aggregation, persistence, and resume. Ordinary state helper nodes
(`state_empty`, `state_get`, `state_set`, and `state_merge`) carry explicit `LoopState` values through the body.

## Supported nested shapes

Nested `For` boundaries are supported recursively when each inner boundary has its own direct linkage and matching
`ForReturn`. The inner final collection may feed the parent return directly or through an ordinary parent-scoped
continuation. Independent inner loops must all feed one explicit fan-in continuation; collection concatenation, zipping,
or Cartesian semantics come from the connected collection operation, not from loop scheduling.

A bounded internal `Iterate` is supported only when one `Collect` collapses its item dimension before the parent
`ForReturn`:

```text
For.item -> Iterate.collection
Iterate.item -> body -> Collect.item
Collect.collection -> ForReturn.output
```

Unsupported shapes, including independent iterator-derived body inputs, mixed nested `For`/`Iterate` bodies, escaping
body paths, ambiguous returns, and arbitrary cyclic graphs, are rejected before execution.

## Persistence and validation

Prepared execution nodes, source/prepared mappings, iteration paths, results, indegrees, and finalized loop contexts
are persisted through `GraphExecutionState`. Runtime-only queues and metadata are rebuilt when state is rehydrated.
Finalization is keyed by the loop source and its parent iteration path so nested or repeated contexts cannot mix output
collections or state.

The frontend and backend validate the same boundary rules. Saved workflows preserve node types, field handles, and the
direct linkage edge. The current invocation templates provide output-scope metadata when a workflow is loaded. The
editor's boundary overlay and contextual `ForReturn` picker are presentation aids; they do not replace whole-graph
validation.

Collection helpers are ordinary explicit nodes: `CollectionConcat` preserves left-to-right order and accepts unequal
lengths, `CollectionZip` requires equal lengths, and `CollectionCartesian` produces deterministic left-major/right-minor
pairs with a 100,000-pair limit. They do not add implicit loop dimensions.
3 changes: 3 additions & 0 deletions docs/src/content/docs/features/Workflows/editor-interface.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,9 @@ The screenshots below aren't examples of complete functioning node graphs, but r
</TabItem>

<TabItem label="Iteration & Batching" icon="list-format">
For a guide to the newer collection-based **For** and **ForReturn** nodes, including loop state and nested loops,
see the [Loop Nodes guide](./loop-nodes).

### Defined & Random Seeds
It is common to want to use both the same seed (for continuity) and random seeds (for variety). To define a seed, simply enter it into the **'Seed'** field on a noise node. Conversely, the **RandomInt** node generates a random integer between 'Low' and 'High', and can be used as input to the 'Seed' edge point on a noise node to randomize your seed.

Expand Down
6 changes: 6 additions & 0 deletions docs/src/content/docs/features/Workflows/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ You can read more about nodes and how to use the node editor by checking out the
href="../workflows/editor-interface"
/>

<LinkCard
title="Loop Nodes"
description="Repeat workflow steps over collections, carry state between iterations, and combine loop results."
href="../workflows/loop-nodes"
/>

## Downloading New Nodes

To download a new node and enhance your workflows with new features, visit our list of Community Nodes. These are nodes that have been created by the community, for the community.
Expand Down
224 changes: 224 additions & 0 deletions docs/src/content/docs/features/Workflows/loop-nodes.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
---
title: Loop Nodes
description: Repeat workflow steps over a collection and optionally carry values from one iteration to the next.
sidebar:
order: 4
lastUpdated: 2026-08-30
---

Loop nodes let a workflow repeat the same group of steps for every item in a collection. For example, a workflow can
process every image found by a detector, try several seeds, or build a collection of results one item at a time.

The main loop uses two nodes:

- **For** starts the loop and provides one collection item at a time.
- **ForReturn** marks the end of the loop body and sends the result back to the loop.

## The Basic Pattern

Connect the nodes in this order:

```text
Collection producer -> For.collection
For.loop_linkage - - - - - - - - - - - - - - - - - > ForReturn.loop_linkage
For.item -> work nodes -> ForReturn.output
For.output_collection -> nodes after the loop
```

The `For` node has one iteration for each item in its `collection`. During an iteration, its `item`, `index`, `total`,
and `state` outputs contain values for that iteration. The work nodes run, then `ForReturn` closes that iteration.

The `For` node does not send its per-item `item` output directly to nodes after the loop. To run a node once after all
iterations finish, connect that node to `For.output_collection` or `For.final_state`.

The editor draws a dashed green boundary around the loop body. This is a visual guide showing which nodes repeat; the
boundary does not carry data. The dashed green line between `For.loop_linkage` and `ForReturn.loop_linkage` is a required
association, not a data value. The remaining values move through the ports and edges you connect.

The loop is not executable until its two boundary nodes are paired. Connect `For.loop_linkage` directly to the matching
`ForReturn.loop_linkage`. When you add `ForReturn` from an iteration-output connection in the node picker, the editor can
create this association for you. Replacing either boundary node requires pairing the replacement again.

### A Simple Example

To process every image in a collection:

```text
Image collection -> For.collection
For.item -> Image Processor.image
Image Processor.image -> ForReturn.output
For.output_collection -> Save or display node
```

The processor runs once per image. The final `output_collection` contains the processed images in the same order as the
input collection.

`ForReturn.output` may be disconnected when the result is carried through loop state instead. A body still needs a
dependency into `ForReturn`, such as a state update or a scalar `continue_condition`; there is no separate completion
port for effects-only bodies.

## What The For Outputs Mean

| Output | Meaning | Use it for |
| --- | --- | --- |
| `loop_linkage` | The association port for the matching `ForReturn` | Pairing the loop boundaries; it is not data |
| `item` | The current collection item | The work done during this iteration |
| `index` | The zero-based position of the current item | Labels, counters, or position-based logic |
| `total` | The collection length | Progress or position-based logic |
| `state` | The current loop state | Values carried from earlier iterations |
| `output_collection` | All values returned through `ForReturn.output` | Work that should happen after the loop |
| `final_state` | The state from the last completed iteration | Reading accumulated values after the loop |

The `item`, `index`, `total`, and `state` outputs belong inside the loop body. The `output_collection` and `final_state`
outputs become available after the loop finishes. `loop_linkage` is only for pairing the two boundary nodes.

## Returning Results

`ForReturn` closes the loop body. It has four useful inputs:

- **Output** adds one value to `For.output_collection` for the current iteration.
- **State** supplies the state for the next iteration.
- **Continue Condition** controls whether another item should be processed. It continues by default; connecting `False`
stops after the current iteration.
- **Loop Linkage** pairs this node with its owning `For`. It must be connected directly to `For.loop_linkage`; it does
not provide a value to the loop body.

The `Loop Linkage` connection is separate from the body data connections. It tells the engine which `ForReturn` closes
which `For`, which matters when loops are nested or several loop boundaries are present in one workflow.

## Carrying Values With State

State is an optional set of named values that travels from one iteration to the next. It is useful for a running sum, a
changing parameter, or several values that must be updated together.

The state helper nodes make this easier:

- **Empty Loop State** creates a blank state.
- **Get Loop State Value** reads a value by name. Its `default` input is used when that name has not been set yet.
- **Set Loop State Value** returns a copy of the state with one named value changed.
- **Merge Loop State Values** returns a copy with several named values changed.

A running value usually looks like this:

```text
For.state -> Get Loop State Value.state
For.item -> Add or other calculation
Get.value + For.item -> calculation
calculation -> Set Loop State Value.value
Set Loop State Value.state -> ForReturn.state
For.final_state -> Get Loop State Value.state (after the loop)
```

Set the `default` on `Get Loop State Value` when the value does not exist during the first iteration. For example, use
`0` for the initial value of a running sum.

State is separate from `output_collection`:

- Use `output` when each iteration produces a result that should be collected automatically.
- Use state when the loop needs to remember or update a value while it runs.
- Use both when the loop needs per-item results and an accumulator.

## Making Collections For A Loop

`For` accepts one collection input. Use collection-producing nodes before it when the values need to be calculated.

### Range Nodes

- **Integer Range** creates values from `start` up to, but not including, `stop`.
- **Integer Range of Size** creates a requested number of values.
- **Random Range** creates a collection of random integers using its seed and bounds.

For example:

```text
Integer Range of Size -> For.collection
For.item -> work nodes
```

This is useful when the loop should run a known number of times. The integer itself can be used as a seed, index, or
ordinary numeric input.

### Combining Collections

These nodes make the collection relationship explicit before the loop:

- **Concatenate Collections** puts every item from the first collection before every item from the second. The two
collections may have different lengths.
- **Zip Collections** pairs items by position. Both collections must have the same length; a mismatch is an error.
- **Cartesian Product of Collections** creates every possible pair, one item from each collection. The collections may
have different lengths. The result is limited to 100,000 pairs.

Choose based on the relationship between the items:

```text
Concatenate: first phase, then second phase
Zip: first[0] with second[0], first[1] with second[1], ...
Cartesian: every item in first with every item in second
```

An empty input to `Concatenate` is harmless. An empty input to `Zip` is valid when both inputs are empty. An empty input
to `Cartesian Product` produces no pairs.

## For And Iterate

Both `For` and `Iterate` repeat work over a collection, but they serve different purposes:

- Use **Iterate** when a normal workflow needs to expand a collection into separate executions and **Collect** when those
executions should be gathered again.
- Use **For** when the repeated work has a clear beginning and end, needs loop state, may stop early, or should expose a
final collection and final state.

An `Iterate` can also be used inside a `For` for a bounded inner pass. The inner values must be gathered by one
`Collect` before the outer `ForReturn`:

```text
For.item -> Iterate.collection
Iterate.item -> inner work -> Collect.item
Collect.collection -> ForReturn.output
```

The outer loop waits for the inner `Iterate` and `Collect` to finish, then returns one collection for that outer item.
Other arrangements, such as multiple internal `Iterate` nodes or an independent external `Iterate` feeding a `For` body,
are not supported.

## Nested For Loops

A `For` can contain another `For`. The inner loop must finish before the outer loop returns its current item:

```text
OuterFor.item -> InnerFor.collection
InnerFor.item -> inner work -> InnerForReturn.output
InnerFor.output_collection -> OuterForReturn.output
```

Each `For` must have its own matching `ForReturn`. The editor and saved workflow keep those pairings separate, so state
from an inner loop does not automatically become state for the outer loop. Connect values explicitly when they need to
cross a loop boundary.

Multiple inner loops can be used as siblings when their final collections feed an ordinary collection-combining node.
That node defines whether those collections are concatenated, zipped, or combined as a Cartesian product. The workflow
does not choose one of those meanings automatically.

## Empty Collections And Early Stop

An empty collection is a successful loop with zero body iterations:

- `output_collection` is empty.
- `final_state` is the provided initial state, or an empty state.
- No body node runs.

Set `ForReturn.continue_condition` to `False` when the loop should stop after the current item. The results and state
from completed iterations remain available through the normal final outputs.

## Common Wiring Mistakes

- Connect the collection to `For.collection`, not to `For.item`.
- Connect `For.loop_linkage` directly to the matching `ForReturn.loop_linkage`.
- Connect body work to `ForReturn.output` when its results should be collected.
- Connect after-loop work to `For.output_collection` or `For.final_state`, not to `For.item` or `For.state`.
- Give each nested `For` its own `ForReturn`.
- Use `Zip Collections` only when the input lengths must match.
- Use `Cartesian Product of Collections` carefully: large inputs can produce many pairs.

For the execution and validation rules behind these connections, see the contributor reference:
[`Loop Nodes Architecture`](../../contributing/loop-nodes).
Loading
Loading