# Loops (/docs/workflows/loops)

> For the complete documentation index, see [llms.txt](https://docs.tandemcx.ai/llms.txt). Any page on this site can be fetched as Markdown by appending `.md` to its URL.

A loop walks a JSON array one element at a time and runs a block of nodes for each one.
Before every pass it writes the current element to a variable you name, so the body reads
the element with the same `$(name)` syntax as any other variable.

There are two forms. They iterate identically: the difference is only how control gets
back to the loop node for the next element.

## Two forms

**Loop** is a single node. You wire its `onIterate` exit to the first node of the body,
and you wire the end of the body back to the Loop node yourself. That back edge is the
iteration. The body is an ordinary run of nodes on the canvas, so nothing constrains where
it goes, and a body with no path back to the Loop node runs once and stops on the first
element.

**Loop Group** is a container. You drag the body inside the group's box, and membership
rather than an edge is what returns control: when a node inside the group reaches an exit
that is wired to nothing, the flow goes back to the group node and the next element
starts. You do not draw a back edge at all.

Reach for **Loop Group** when the body is more than a couple of nodes, when you want it
visually boxed, or when you need to leave the loop early (only the container form has a
`break` exit). Reach for **Loop** when you would rather see the cycle drawn as an explicit
edge, or when the body is a single node.

### Exits

| Exit           | Loop | Loop Group | When it's taken                                                                                                                                                                         |
| -------------- | ---- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onIterate`    | yes  | yes        | Once per element, before the body runs.                                                                                                                                                 |
| `onFinished`   | yes  | yes        | Every element processed, or the array was empty.                                                                                                                                        |
| `break`        | no   | yes        | A **Loop Break** node inside the group ran.                                                                                                                                             |
| `invalidInput` | yes  | yes        | The `Array` field didn't resolve to an array, **Maximum Iterations** is below 1, the item variable can't be written, or **Maximum Iterations** was reached with items left.             |
| `invalidInput` | yes  | yes        | The array holds more than 1000 elements. Taken on the first pass, before any element runs — see [Limits and defaults](#limits-and-defaults).                                            |
| `invalidInput` | yes  | yes        | The loop lost its iteration state mid-walk. The message names how many elements had been processed; the rest were not. See [When a loop loses its place](#when-a-loop-loses-its-place). |

## Fields

- **Array** — the list to walk. A variable holding a JSON array, or a JSONPath that
  resolves to one, for example `$(order.items)`. A value that isn't an array takes
  `invalidInput`. An empty array takes `onFinished` without running the body once.
- **Item Variable** — the name the current element is written to before each pass.
- **Maximum Iterations** — a runaway guard. Defaults to `100`, and must be between `1`
  and `1000`.

## The item variables

Each pass writes three variables, all derived from the name you put in **Item Variable**.
For `invoice`:

- `$(invoice)` — the current element.
- `$(invoice_index)` — its zero-based position in the array.
- `$(invoice_count)` — the length of the whole array.

They are ordinary conversation variables in the same bag as everything else, not a
reserved namespace. Two consequences worth knowing:

- A loop nested inside another loop does not shadow its parent, as long as the two loops
  use different item variable names. Both sets stay readable in the inner body. Reusing one
  name overwrites the outer element on the inner loop's first pass, and the outer loop
  keeps running against the overwritten value. Validation rejects the reuse only when the
  outer loop is a **Loop Group**, because containment is the only nesting the editor can
  read: one Loop node driving another through ordinary edges gets no warning. Give every
  loop in a nest its own item variable name.
- The variables are not cleared when the loop finishes. After `onFinished`, `$(invoice)`
  still holds the last element. Don't read them downstream of the loop expecting them to
  be empty.

## Maximum Iterations is a fault, not a batch size

Reaching **Maximum Iterations** with items still unprocessed takes the `invalidInput`
exit. It does not take `onFinished`.

That is deliberate: a loop that dropped most of its work should not report success and let
the downstream branch act on it. The message names the loop, the limit, and how much was
left, so you can tell "my cap is too small" from "my data is wrong":

```
Loop Group "processInvoices" stopped at its maximum of 100 iterations with 42 of 142 items unprocessed.
```

Wire `invalidInput` on the loop node if you want to handle that case. Left unwired inside a
Loop Group it is claimed by the group as an ordinary end-of-iteration hop, which is not
what you want for a fault.

## The node-execution budget is the real ceiling

**Maximum Iterations** is usually not what stops a long loop. Every workflow run has a
total node-execution budget, `500` by default, and each pass of a loop spends one
execution per node in the body plus one for the loop node itself. So a loop with an
`n`-node body completes at most about:

```
500 / (n + 1) iterations
```

A four-node body costs five executions per pass, so the default cap of 100 and the budget
land in roughly the same place. A five-node body costs six per pass and gets about 83
iterations, well short of a cap of 100. A ten-node body gets about 45.

Treat the figure as an estimate, accurate only when the body runs every node on every pass.
A body that branches runs fewer nodes per pass and gets further. Everything else on the run
spends the same budget, so nodes before the loop, a nested loop, and the loop node's own
final pass all mean fewer iterations than the division suggests.

When you verify or publish a Loop Group whose cap times its body size exceeds the budget,
validation warns and gives you the number:

```
Loop Group 'processInvoices' may run 100 iterations over the 5 nodes inside it. That is
6 node executions per pass (the 5 inside plus the group itself) and 600 in all, against
the 500 one workflow run may execute by default. It stops after 83 iterations, on the
run's node-execution limit rather than on its own maximum. Lower 'maxIterations' to 83,
or move nodes out of the group.
```

The warning is only produced for the container form. A plain **Loop** node's body is
author-drawn edges, so nothing can count it, but the budget applies to it just the same.

The budget is a per-organization limit and yours may have been raised or lowered; the
warning always does its arithmetic against the default.

## Leaving a loop early

Drop a **Loop Break** node inside a Loop Group and wire the body to it. Running it
abandons the remaining elements and sends the flow out the group's `break` exit.

Loop Break has no fields and nothing to wire onward: the group decides where the flow goes
next, so wire the group's `break` exit rather than an exit on the Loop Break node. Wiring
the group's `break` exit is required even when the group holds no Loop Break today.

Loop Break walks past a Try / Catch on its way out, so a Loop Break inside a Try / Catch
inside a Loop Group still leaves the loop.

The plain **Loop** node has no break, and there is no safe way to walk out of its body by an
edge that stays inside whatever encloses the loop. Nothing discards the loop's iteration
state on that route: for the rest of the run the per-node repeat guards stay suppressed on
every node that follows, and re-entering the same Loop node later resumes the array where
it left off instead of starting at the first element. The one exception is a plain Loop
running inside a Loop Group, where an edge that leaves the group discards the group's
iteration state and the plain loop's along with it.

Use a Loop Group with a Loop Break when you need to leave early. If you have to stay on the
plain Loop node, wire the early-exit branch back to the Loop node and use a Condition in
the body to skip the work for the elements you no longer care about: the loop still visits
every one of them, and spends budget doing it, but every way it can end — running out of
elements, or faulting on **Maximum Iterations** — discards its iteration state on the way
out. The exception is the run's node-execution limit. A workflow killed there leaves the
state behind until the run is torn down.

## What validation checks

Verify and publish report these; saving a draft does not.

Errors:

- A **Loop** whose `onIterate` exit has no path back to the Loop node, so it runs its body
  once and stops on the first element. Also an unwired `onIterate`.
- A **Loop Group** with `onIterate`, `onFinished`, or `break` unwired. All three are
  required. `onIterate` must point at a node directly inside the group, not at one inside a
  nested group.
- An empty **Item Variable**, or a name a workflow can't set: anything in the `sys.`
  namespace, a name containing a dot or a space, or a reserved root. Use letters, numbers,
  `-` and `_`.
- An **Item Variable** that matches the one used by a Loop Group it sits inside.
- A **Maximum Iterations** that is missing, or outside `1` to `1000`.
- A **Loop Break** that isn't inside a Loop Group. There's no loop for it to stop, and at
  runtime it takes `invalidInput` instead of breaking.
- A **Loop Break** with any exit other than `invalidInput` or `fallback` wired.

Warnings:

- A group with no nodes inside it.
- A Loop Group whose `onFinished` or `break` exit points back inside the group. That node's
  unwired exit returns to the group, which reopens the frame it just popped and starts over
  from the first element.
- The node-execution budget warning above.

A field holding a `$(...)` reference is left alone by the numeric and name checks, since
the value isn't known until the node runs.

## Limits and defaults

|                                  | Value                            |
| -------------------------------- | -------------------------------- |
| **Maximum Iterations** default   | 100                              |
| **Maximum Iterations** range     | 1 to 1000                        |
| Longest array a loop can walk    | 1000 elements                    |
| Node executions per workflow run | 500 by default, per organization |
| Group nesting depth              | 5                                |

An array longer than 1000 elements takes `invalidInput` on the first pass rather than
processing the first 1000. Page the data in your integration or database query instead.

## When a loop loses its place

A running loop keeps its position — the array, the current index, and the count — in
short-lived storage, separate from the workflow itself. If that position is gone when the
loop asks for the next element, the loop takes `invalidInput` with a message naming how
far it got:

> Loop Group "Send reminders" lost its iteration state after 12 of 40 items; the rest were
> not processed.

It faults instead of finishing on purpose. Taking `onFinished` would tell you the loop was
done after 12 of 40, and everything wired downstream would act on that. Wire `invalidInput`
somewhere that can tell the difference between a loop that finished and a loop that
stopped, rather than joining it back into the `onFinished` path.

You are unlikely to see this on a workflow that runs straight through. The position is
kept alive for as long as the workflow's own paused state whenever a node inside the body
suspends — a Send SMS waiting for a reply, a Play Message waiting for digits, a Delay — so
an ordinary wait inside the body is safe. It remains possible across an unusually long
pause or if the position is evicted under storage pressure.

## Next steps

<Cards>
    <Card
        title="Variables & Data"
        href="/docs/workflows/variables-and-data"
        description="Store data and reference it across nodes."
    />
    <Card
        title="Error Handling"
        href="/docs/workflows/error-handling"
        description="Faults inside a loop, and Try / Catch."
    />
    <Card
        title="Node Reference"
        href="/docs/workflows/reference"
        description="Fields and exits for every node."
    />
</Cards>
