Loop loop
Iterate over an array and execute the loop body once per item. Injects the current item and index into ExecutionMemory for every iteration. The done port fires after all iterations complete or after an early Break exit.
When to Use
- Process each order in a batch: Loop over an array of orders retrieved from a database query, applying fulfilment logic, sending notifications, and updating statuses for each one individually.
- Send individual emails to each recipient: Loop over a recipient list and dispatch personalised email content per person — each iteration has its own
current_itemwith the recipient's name and preferences. - Update each record from a database result: After a MongoDB or SQL query returns an array of documents, loop over them to apply field updates, enrich with external API data, or write back transformed versions.
- Transform each item in a collection: Apply a normalisation, enrichment, or format-conversion pipeline to every element in an array before writing the results to a downstream system.
- Validate each field in a form submission: Iterate over a dynamic array of form answers to apply per-field validation rules, collecting failures into an error list for a structured validation response.
Configuration
| Field | Required | Description |
|---|---|---|
Items |
Required | Variable name or template expression that resolves to the array to iterate over. This can be a simple variable name (e.g. recipients, resolved as $var.recipients) or a full template expression (e.g. {{ $output.findDocs.documents }}). The value must resolve to a JSON array — a scalar or object will cause a validation error. |
body port never fires. Execution proceeds directly to the done port without error. This means you do not need an IfCondition to guard against empty collections before a Loop node.
Output Ports
| Port | Fires When |
|---|---|
body | Once per iteration — fires for each element in the array in order. current_item and current_index are injected into ExecutionMemory before this port fires. This is where you connect the per-item processing nodes. |
done | After all iterations have completed normally, or immediately after a Break node signals an early exit. Connect post-loop logic here — aggregation, summary notification, or continuation of the workflow. |
error | If the Items expression fails to resolve, or if the resolved value is not an array. This port does not fire for errors inside the loop body — those propagate through the body subgraph's own error handling. |
OutputData Fields
Fields available after the loop via $output.<nodeKey>:
| Field | Type | Description |
|---|---|---|
collection_size | int | Total number of elements in the array. Available immediately after the loop node (including during the done port path). |
current_index | int | The index of the most recently processed item (0-based). On the done port, this equals collection_size - 1 for a completed loop, or the index at which a Break fired. |
items | array | The full resolved array that was iterated. Useful for downstream nodes that need the original collection without re-fetching it. |
Per-Iteration Variables (Injected into ExecutionMemory)
These variables are available inside the loop body on every iteration via $var:
| Variable | Type | Description |
|---|---|---|
current_item | object | The current array element. For an array of objects, access fields directly: {{ $var.current_item.orderId }}. |
current_index | int | The 0-based index of the current iteration. First item is 0, last is collection_size - 1. |
Sample Configuration
Loop over a variable
{
"Items": "recipients"
}
Loop over a node output (MongoDB documents)
{
"Items": "{{ $output.findOrders.documents }}"
}
Loop over a JSON array field from the trigger
{
"Items": "{{ $json.lineItems }}"
}
Sample Output
Per-Iteration Output (Success Port)
The loop fires the success port once per item. Each execution carries the current item's data plus loop metadata.
{
"productId": "PRD-00451",
"sku": "WGT-BLK-L",
"name": "Widget Black Large",
"stock": 142,
"reorderPoint": 50,
"supplierId": "sup_001",
"_loop": {
"currentIndex": 3,
"totalItems": 47,
"isFirst": false,
"isLast": false
}
}
Loop Completed Port
{
"_loop": {
"completed": true,
"totalItems": 47,
"processedCount": 47,
"errorCount": 0,
"durationMs": 2341
}
}
Expression Reference
| Expression | Returns |
|---|---|
{{ $var.current_item }} | Current element — available inside loop body only. |
{{ $var.current_item.fieldName }} | A specific field of the current object element. |
{{ $var.current_index }} | 0-based integer index of the current iteration. |
{{ $output.myLoop.collection_size }} | Total number of items in the array (available on done port). |
{{ $output.myLoop.items }} | The original full array (available on done port). |
Validation Errors
| Error | Cause |
|---|---|
VAL_MISSING_ITEMS | The Items field is empty or whitespace. |
VAL_ITEMS_NOT_ARRAY | The resolved Items expression returned a non-array value (scalar, object, or null). |
Node Policies and GuardRails
- Items must resolve to an array: The expression in the
Itemsfield must evaluate to a JSON array. To loop over MongoDB documents, use{{ $output.findDocs.documents }}not the node output object itself. - Do not mutate the loop array inside the body: The loop array is materialised once at the start. Modifying the source variable inside the loop body does not affect the current iteration sequence — but it can cause confusing state for downstream code. Use a separate accumulator variable instead.
- Large arrays — consider batching: For arrays exceeding 1000 items, consider splitting into batches using a Function node or a pre-processing step before the loop. Very large loops increase workflow execution time and memory pressure.
- Nested loops require separate Loop nodes: To loop inside a loop, add a second Loop node inside the body branch. Each Loop node maintains its own
current_itemandcurrent_indexscope, so nested loops do not interfere. - Always wire the done port: Post-loop logic (aggregation, summary, continuation) must connect to the
doneport, not thebodyport. Connecting continuation logic to the body means it runs once per item, not once after all items.
$var.current_item and $var.current_index are only valid inside the loop body (the subgraph connected to the body port). They are not available on the done port path. On the done port, use $output.loopNodeKey.items to access the full collection.
Pattern Examples
Pattern 1 — Batch Email Dispatch
Load a recipient list from a database query, then loop to send a personalised email to each recipient. After all emails are sent, trigger a completion notification.
MongoDB [key: "getRecipients"]
(find all active subscribers)
└─► Loop [key: "sendEmails"]
Items: {{ $output.getRecipients.documents }}
├─► body ──► EmailSmtp [key: "sendOne"]
│ To: {{ $var.current_item.email }}
│ Subject: "Your monthly report is ready"
│ Body: "Hi {{ $var.current_item.firstName }}, ..."
└─► done ──► SlackNode (notify team: sent {{ $output.sendEmails.collection_size }} emails)
Pattern 2 — Per-Item Validation with Early Exit
Validate each item in a cart. If any item fails stock check, Break exits immediately and routes to an error response. Otherwise the done port proceeds to checkout.
FormTrigger (checkout submitted)
└─► Loop [key: "validateCart"]
Items: {{ $json.cartItems }}
├─► body ──► HttpRequest [key: "checkStock"]
│ GET /inventory/{{ $var.current_item.productId }}
│ └─► IfCondition
│ Condition: {{ $output.checkStock.quantity > 0 }}
│ ├─► true ──► (continue to next iteration)
│ └─► false ──► Break (exit loop immediately)
└─► done ──► [proceed to payment processing]
Pattern 3 — Accumulating Results Across Iterations
Transform each item and accumulate results into a list variable, then use the full list after the loop completes.
VariableAssignment [key: "initResults"]
processedOrders = []
└─► Loop [key: "processOrders"]
Items: {{ $json.orders }}
├─► body ──► Function [key: "transformItem"]
│ (compute enriched order object)
│ └─► VariableAssignment
│ processedOrders = {{ $var.processedOrders }}
│ .concat([$output.transformItem.result])
└─► done ──► HttpRequest (POST /api/orders/bulk)
Body: {{ $var.processedOrders }}