Loop
Run part of the workflow once per item of a list, one item at a time or several at once.
The Loop node (loop) takes a list and runs the nodes on its loop handle — the loop body — once per item, one after another or several items at the same time (batchSize). When every item is done, the run continues from its done handle.
Configuration
| Field | Type | Default | Description |
|---|---|---|---|
sourceArray | string | '' | A {{ }} expression that resolves to a list, for example {{ trigger.body.orders }} or {{ upstream.search.output.rows }}. |
batchSize | integer ≥ 1 | 1 | How many items run at the same time (1 = one after another). Each iteration still gets one item. |
Inside the loop body
| Reference | Value |
|---|---|
{{ loop.item }} | The current item, whatever batchSize is. |
{{ loop.item.<field> }} | A field of the current item. |
{{ loop.index }} | The current iteration, starting at 0. |
In a loop nested inside another loop, loop.* refers to the innermost one. Writes to a variable inside the body last only for that iteration.
Output
Read a body node's results from after the loop: {{ upstream.<body node id>.output.iterations }} is a list of that node's output, one entry per iteration, in order. That is almost always what you want. After the loop, a body node's output is { iterations, loopNodeId }, not the list itself, so {{ upstream.<body node id>.output.0 }} does not exist; use output.iterations.0. An iteration in which the node did not run (its branch was not taken) has no entry, so the list can be shorter than the number of items.
The Loop node's own output is the per-iteration record the run trace shows: a list with one entry per iteration, each an object keyed by body node id ({{ upstream.<loop node id>.output.0.<body node id> }}). A Loop with nothing on its loop handle records { index, item } for each item.
Limits and errors
A Loop node stops after a bounded number of iterations — 100 by default on a self-hosted deployment, configurable via the WORKFLOW_LOOP_ITERATION_CEILING environment variable up to a hard cap of 1000.
Hitting that bound is not a failure. The run ends with status loop_limit_reached, keeps whatever the loop did process, and reports no error — the bound is a declared safety limit, and it held. Treat it as its own outcome in your integration rather than folding it into either completed or failed.
- A Wait or Human Review node cannot go inside a loop body; it fails the loop. Put it before or after the loop (a batch Human Review reviews a whole list in one request).
- A
sourceArraythat does not resolve to a list fails the node. - With
batchSizeabove 1, up to that many iterations run at the same time, and the next group starts when the whole group has finished. Parallel iterations share nothing: each has its own{{ loop.item }},{{ loop.index }}and variable writes, and results are still listed in the source order. Don't rely on one iteration seeing another's effects (a row it wrote, an email it sent); usebatchSize: 1when order matters.
Example
Summarise each attachment of an email, then send one message with every summary:
name: Summarise attachments
description: Summarises each attachment of an incoming email.
graph:
nodes:
- id: mail
type: trigger_email
position: { x: 0, y: 0 }
data:
downloadAttachments: true
pollMinutes: 5
- id: each-attachment
type: loop
position: { x: 280, y: 0 }
data:
sourceArray: '{{ trigger.attachments }}'
batchSize: 1
- id: summarise
type: ai_prompt
position: { x: 560, y: -80 }
data:
llm_slot: summary-model
model_id: gpt-4o-mini
instructions: Summarise this document in three sentences.
inputExpression: '{{ loop.item.text }}'
outputFormat: text
- id: collect
type: edit_fields
position: { x: 560, y: 80 }
data:
rows:
- { id: summaries, name: summaries, value: '{{ upstream.summarise.output.iterations }}' }
keepOnlySetFields: true
edges:
- { id: mail-to-loop, source: mail, target: each-attachment }
- { id: loop-body, source: each-attachment, target: summarise, sourceHandle: loop }
- { id: loop-done, source: each-attachment, target: collect, sourceHandle: done }In a Package
- Travels:
sourceArray,batchSize - Converted: none
- Never carried: none