

For-Each Loop node
For-Each Loop Node
Run the nodes between For-Each and End For-Each once per item of an array or object, with the error handling and result collection you choose.
- Output
- LoopResult on the End For-Each node
- Defaults
- One item at a time, in order
- Limits
- Nesting up to 10 levels
Overview
The For-Each Loop turns a single array or object payload into multiple executions of downstream nodes, one run per item. Instead of building manual loops or complex branching, you define a single source expression, and the pipeline engine fans out the flow automatically. A companion End For-Each node marks the boundary of the loop body.
Typical use cases include:
- Iterating over a list of work orders, sensor events, or measurements in a batch.
- Running the same sub-pipeline for each line item in an order.
- Breaking a large API response into per-item processing without writing imperative loop code.
- Iterating over Read Group results (object keyed by function name) to process each reading individually.
How the Loop Works
The For-Each Loop uses a paired-node model: the For-Each node begins the loop and the End For-Each node marks where it ends. Every node between them is the loop body and executes once per array item, sequentially (item 0 completes fully before item 1 begins). Within a single iteration, body nodes run in parallel where their dependencies allow.
Execution Flow
┌────────────── Loop Body ──────────────┐
│ │
For-Each ──► [Node A] ──► [Node B] ──► End For-Each ──► [After Loop]
│ │
├── item 0 ──► A ──► B ──► End ─────────┤
├── item 1 ──► A ──► B ──► End ─────────┤
├── item 2 ──► A ──► B ──► End ─────────┤
│ │
└───── LoopResult (results, counts) ────┘
- The For-Each node evaluates the Source Expression and determines the iteration count. When the source is an object, it iterates over key-value pairs in alphabetical key order.
- For each item in the array, all loop body nodes execute in dependency order, from For-Each through to End For-Each. Body nodes within a single iteration can run in parallel (up to 4 workers) when they have no mutual dependencies.
- The End For-Each node acts as a pass-through that marks the loop boundary; its output for each iteration becomes that iteration's result.
- After all iterations complete (or the loop stops early due to errors), the End For-Each node produces a LoopResult object and execution continues with nodes connected after it.
When you drag a For-Each node onto the canvas, an End For-Each node is automatically created and connected 400 px to the right. You only need to place your processing nodes between them.
Iterating Over Objects
When the source expression resolves to an object (e.g., the output of a Read Group node), the For-Each node converts it to a list of key-value pairs and iterates over them:
$item: the value for the current key$key: the key name (e.g.,"voltage","vibration"). Resolves to an empty string""when iterating over arrays.$index: the numeric iteration index (0, 1, 2, …)- Iteration order is alphabetical by key for deterministic behavior
Example (Read Group → For-Each):
A Read Group node outputs:
{
"results": {
"voltage": { "value": 393.11, "success": true },
"vibration": { "value": 4.26, "success": true }
},
"_metadata": { "connectionId": "...", "total": 2 }
}
Set the For-Each source expression to {{ $node["Read Group"].result }} to iterate over the results:
| Iteration | $key | $item | $index |
|---|---|---|---|
| 1 | "vibration" | { "value": 4.26, "success": true } | 0 |
| 2 | "voltage" | { "value": 393.11, "success": true } | 1 |
Inside the loop body you can access {{ $key }} for the tag name and {{ $item.value }} for the reading.
Loop Variables
Inside the loop body, the following expression variables are available:
| Variable | Type | Description |
|---|---|---|
$item | any | The current array element or object value being processed (always refers to the innermost loop). |
$index | number | Zero-based index of the current iteration (0, 1, 2, …) for the innermost loop. |
$key | string | The object key for the current entry (only when iterating over objects; empty string for arrays). |
These variables are scoped to the loop body and cleared after the loop completes.
Named Loop Variables (Item Variable Name)
If you set the optional Item Variable Name parameter (e.g., order), two additional variables become available:
| Variable | Type | Description |
|---|---|---|
$order | any | Alias for the current item; identical to $item but named for clarity. |
$orderIndex | number | Alias for the current index; identical to $index but named. |
Named variables are particularly useful for readability, self-documenting pipelines, and nested loops. The generic $item / $index always refer to the innermost loop, while named variables persist by name across nesting levels; inner loops can access outer loop variables by name.
Example (accessing variables in a Set or Code node inside the loop):
{{ $item.name }} — property of the current item
{{ $item }} — the entire current item
{{ $index }} — current iteration index (0-based)
{{ $key }} — object key (e.g., "voltage") when iterating over objects
{{ $order.id }} — same as $item.id (when itemName = "order")
{{ $orderIndex }} — same as $index (when itemName = "order")
Configuration Reference
For-Each Node Parameters


For-Each configuration panel
Source Data (required)
| Type | Expression (string) |
| Default | "" (empty) |
| Placeholder | {{ $node["Data"].result }} |
The expression that resolves to the array or object to iterate over. This is the only required parameter. When the source is an array, each element becomes $item. When the source is an object, each key-value pair is iterated in alphabetical key order: $item is the value and $key is the key name.
Expression examples:
| Expression | Description |
|---|---|
{{ $node["API"].result }} | Array from an API response node |
{{ $node["Query"].result }} | Extract the rows from a query result |
{{ $node["ReadGroup"].result }} | Results object from a Read Group node (iterates key-value pairs) |
{{ $trigger.result }} | Array or object from the trigger's payload directly |
{{ filter($node["Orders"].result.list, #.status == "active") }} | Filtered subset using a built-in function |
Item Variable Name (optional)
| Type | Text (alphanumeric + underscore only) |
| Default | "" (empty; uses $item / $index only) |
| Max Length | 30 characters |
Give the loop item a descriptive name. When set to e.g. order, the variables $order and $orderIndex become available in addition to $item and $index. See Named Loop Variables above.
Error Mode
| Type | Select |
| Default | stopOnFirst |
Controls how the loop handles failures during iteration.
| Value | Label | Behavior |
|---|---|---|
stopOnFirst | Stop on First Error | Stops the loop immediately when any iteration fails, and the pipeline run is reported as failed. The End For-Each node still emits its result marked partial: true with a stopReason (the same shape the error-threshold stops produce), so nodes wired after the loop (cleanup, notifications, compensation) run even on the failure path. This is the default and safest option. |
continue | Continue on Error | Skips the failed item and continues with the remaining items. Failed items are recorded in the output's errors array. |
continueWithRetry | Continue with Retry | Retries the failed item with exponential backoff before skipping it. If all retries fail, records the error and continues to the next item. See Item Retry Behavior below. |
collectAll | Collect All Errors | Processes every item regardless of failures and collects all errors at the end. Useful for audit or reporting scenarios where you need a full picture. |
Error Thresholds
These fields appear when Error Mode is set to anything other than Stop on First Error:
| Parameter | Type | Default | Range | Description |
|---|---|---|---|---|
| Max Error Count | number | 0 | 0-1,000 | Maximum number of errors before stopping the loop early. 0 means unlimited; the loop will process all items. |
| Error Threshold (%) | number | 0 | 0-100 | Stop the loop if the error percentage exceeds this threshold. For example, 50 means stop if more than half the processed items have failed. 0 disables threshold checking. |
| Partial Results | toggle | true (on) | - | When enabled, the loop outputs all successful results collected so far even if the loop stopped early due to errors. When disabled, early stops produce no result data. |
Error thresholds apply to all non-stopOnFirst modes, including Collect All Errors. Even in collectAll mode, the loop will stop early if a threshold is exceeded.
Result Mode
| Type | Select |
| Default | none |
Controls what iteration results are collected in the output.
| Value | Label | Memory | Description |
|---|---|---|---|
none | No Results | O(1) | Fire-and-forget: only tracks count, successCount, and errorCount. Best for side-effect loops (e.g., sending notifications). This is the default. |
errorsOnly | Errors Only | O(errors) | Discards successful results: results stays empty; failures are reported in the errors array, exactly as under Error Mode continue. Useful for debugging without the overhead of collecting all results. |
last | Keep Last Only | O(1) | Only keeps the last successful iteration's result. Useful when only the final computation matters. |
all | Collect All Results | O(n) | Stores every iteration's result in the results array. Required when downstream nodes need the full set of processed data. |
Collect All Results stores every iteration's output in memory. For large arrays (1,000+ items), prefer No Results, Errors Only, or Keep Last Only to avoid excessive memory usage.
Item Retry Behavior
When Error Mode is set to Continue with Retry, the loop retries each failed item using exponential backoff before moving on. The retry uses the following default parameters:
| Setting | Default | Description |
|---|---|---|
| Max Attempts | 3 | Maximum retry attempts per failed item. |
| Initial Delay | 1,000 ms | Wait time before the first retry. |
| Max Delay | 120,000 ms (2 min) | Upper bound for the backoff delay. |
| Multiplier | 2.0 | Exponential backoff multiplier. Each retry waits initialDelay × multiplier^attempt. |
| Jitter Factor | 0.1 | Adds ±10% randomness to avoid thundering-herd retries. |
Only transient errors (network timeouts, connection resets, rate limits, 503/429 responses) are retried. Permanent errors (invalid input, authentication failures, 400/401/403/404 responses) skip remaining retries immediately.
Execution Settings
Both For-Each and End For-Each nodes have an Execution Settings tab with standard options:
| Setting | Options | Default | Description |
|---|---|---|---|
| Timeout (seconds) | number | No loop-wide limit | For the For-Each: the most the whole loop may take, every item together (1-3600). Empty means no loop-wide limit: each step inside the loop keeps its own timeout on every item, and the pipeline timeout still applies. For End For-Each: the pipeline default. |
| Retry on Timeout | Pipeline Default / Enabled / Disabled | Pipeline Default | Whether to retry the node-level execution on timeout. |
| Retry on Fail | Pipeline Default / Enabled / Disabled | Pipeline Default | Whether to retry the node-level execution on failure. When Enabled, shows Advanced Retry Configuration. |
| On Error | Pipeline Default / Stop Pipeline / Continue Execution | Pipeline Default | Behavior when the node fails after all retries. |
The Execution Settings apply to the For-Each node itself (e.g., overall loop timeout), not to individual iterations. For per-item retry behavior, use the Error Mode set to Continue with Retry in the Configuration tab.
When a timeout stops the loop
If the For-Each timeout, the pipeline timeout or a Stop ends the loop while items are still left, the For-Each node fails, whatever the Error Mode. Error modes decide what happens when an item fails; here no item failed, the loop ran out of time. The error names the limit that was reached and how many items ran, for example Stopped after 5m0s: the For-Each timeout was reached. 23 of 40 items ran, 17 did not. The results collected so far stay on End For-Each, marked partial: true with the same text as stopReason, and the nodes after the loop still run. With On Error set to Continue Execution, the run ends completed with errors. A loop stopped this way is never retried, because a retry would run the finished items again.
When a step fails for some items
A step inside the loop that fails for some items, whether the loop's Error Mode carries on past them or the step's own On Error is Continue Execution, ends completed with errors, and so does the run. Its error says how many items failed and names the first one, for example Failed for 1 of 22 items (first: item 9: File not found in bucket: T10F UNB INHOUSE STUD). If it failed for every item it ran, the step is failed. Items a Condition routed away from are skipped, not failed.
In Execution History, a failed item keeps its records like any other: open the step, go to that item, and you see the error and the item that caused it. This works on a step that failed for every item too: the item picker sits under its error. Steps after the failed one show the item as skipped, or have no record when they never ran for it.
When a step's On Error is Continue Execution
With On Error set to Continue Execution (on the step, or as the pipeline default), a failing step doesn't stop its item: the steps after it still run for that item. The loop still counts the item as failed. It adds to errorCount instead of successCount and appears in the End For-Each errors array. The error names the first step that failed for that item, for example S3 Fetch Object: object discovery failed: no objects found matching pattern: T10F LH FRONT DOOR OP35.*\.CSV$. A For-Each with any failed item ends completed with errors.
What the loop does next follows the step's setting, not the Error Mode:
- Max Error Count and Error Threshold (%) count these items and stop the loop when reached.
- Stop on First Error does not stop the loop for them, and Continue with Retry does not run the item again. The step was set to carry on, and a retry would run the item's other steps again.
- Under Collect All Results or Keep Last Only, the item's output is still collected.
partial stays false when every item ran. It only says the loop stopped before the end.
An inner For-Each that ends completed with errors does not fail the outer loop's item. Each loop counts the items it iterates over.
End For-Each Node


End For-Each node
The End For-Each node marks the end of the loop body. It has no configurable parameters beyond a name and the standard Execution Settings.
Behavior
- Pass-through: During each iteration, the End For-Each node receives the output from the last node in the loop body and passes it through unchanged, adding
_loopmetadata. - Result aggregation: After all iterations complete, the For-Each executor stores the aggregated LoopResult on the End For-Each node. This is the data available to downstream nodes.
- Boundary marker: The pipeline engine uses the End For-Each node to determine which nodes belong to the loop body vs. which nodes run after the loop.
How to Set Up
- Add a For-Each node to start the loop (this auto-creates the End For-Each).
- Place your processing nodes between For-Each and End For-Each.
- Connect the last processing node to End For-Each.
- Connect End For-Each's output to any nodes that should run after the entire loop finishes.
For-Each ──► [Process A] ──► [Process B] ──► End For-Each ──► [Post-Loop Node]
Output Structure (LoopResult)
After all iterations complete, the End For-Each node produces a LoopResult object. This is the data available to downstream nodes via expressions like {{ $node["End For-Each"].result }}.
Per-Iteration Result Structure
Each entry in the results array is a map keyed by body node names, plus _loop metadata. This means you get the output of every body node from each iteration, not just the last node's output.
Single-body-node example (one "Convert" node in the loop body, Result Mode = Collect All Results):
{
"results": [
{
"_loop": { "index": 0, "total": 2 },
"Convert": { "name": "Alice", "processed": true }
},
{
"_loop": { "index": 1, "total": 2 },
"Convert": { "name": "Bob", "processed": true }
}
],
"count": 2,
"successCount": 2,
"errorCount": 0,
"partial": false
}
Multi-body-node example (two nodes "Enrich" and "Validate" in the loop body):
{
"results": [
{
"_loop": { "index": 0, "total": 2 },
"Enrich": { "name": "Alice", "score": 95 },
"Validate": { "valid": true, "errors": [] }
},
{
"_loop": { "index": 1, "total": 2 },
"Enrich": { "name": "Bob", "score": 72 },
"Validate": { "valid": true, "errors": [] }
}
],
"count": 2,
"successCount": 2,
"errorCount": 0,
"partial": false
}
Each key in a result entry is the name of a body node (or its ID if unnamed). Nodes that were skipped during an iteration (e.g., due to condition routing) are excluded from that entry.
Run with Errors
When errors occur (Error Mode = Continue, Result Mode = Collect All Results):
{
"results": [
{
"_loop": { "index": 0, "total": 3 },
"Process": { "name": "Alice", "processed": true }
}
],
"count": 3,
"successCount": 1,
"errorCount": 2,
"errors": [
{
"index": 1,
"item": { "name": "Bob" },
"error": "Connection timeout",
"code": "CONN_TIMEOUT",
"attemptCount": 1
},
{
"index": 2,
"item": { "name": "Charlie" },
"error": "Service unavailable",
"code": "SERVICE_UNAVAILABLE",
"attemptCount": 1
}
],
"partial": false
}
Empty Array
When the source array is empty, the For-Each node completes immediately with no iterations:
{
"results": [],
"count": 0
}
Field Reference
| Field | Type | Present | Description |
|---|---|---|---|
results | array | Always | Iteration outputs collected from the End For-Each node. Each entry is a map keyed by body node names with _loop metadata. Contents depend on Result Mode: null for none and errorsOnly (nothing is collected), single item for last, all items for all. |
count | number | Always | Total number of items in the source array (attempted iterations). |
successCount | number | Always | Number of iterations that completed successfully. |
errorCount | number | Always | Number of iterations that failed. |
partial | boolean | Always | true if the loop stopped early (error thresholds, max error count, or a timeout or Stop) before processing all items. |
errors | array | Only when errors occur | Details of each failed iteration. Each entry contains index, error message, error code, attemptCount, and optionally the original item. Omitted entirely when there are no errors. |
stopReason | string | Only on early stop | Reason the loop stopped early (e.g., "max error count reached (5)"). Omitted entirely when the loop completes normally. |
Accessing Results in Downstream Nodes
After the loop, you can reference the results in any downstream node using the End For-Each node name:
{{ $node["End For-Each"].result }} — full results array
{{ $node["End For-Each"].result[0]["Process"].result.name }} — first result's "Process" node output
{{ $node["End For-Each"].result[0]._loop }} — loop metadata of first result
{{ $node["End For-Each"]._metadata.count }} — total items attempted
{{ $node["End For-Each"]._metadata.successCount }} — successful iterations
{{ $node["End For-Each"]._metadata.errorCount }} — failed iterations
{{ $node["End For-Each"]._metadata.partial }} — whether results are incomplete
{{ $node["End For-Each"]._metadata.errors }} — error details (only if errors occurred)
{{ $node["End For-Each"]._metadata.stopReason }} — early stop reason (only if stopped early)
The aggregated LoopResult is stored on the End For-Each node, not the For-Each node. When writing expressions, reference the End For-Each node name (e.g., $node["End For-Each"]).
Nested For-Each Loops
For-Each loops can be nested inside other For-Each loops, up to a maximum depth of 10 levels. Nested loops are useful when you need to iterate over arrays within arrays (e.g., orders containing line items).
How Nesting Works
Outer For-Each ──► Inner For-Each ──► [Process] ──► Inner End ──► Outer End ──► [After]
│ │ │ │
├── order 0 ├── lineItem 0 ──► Process ────┤ │
│ ├── lineItem 1 ──► Process ────┤ │
│ └── lineItem 2 ──► Process ────┘ │
├── order 1 ├── lineItem 0 ──► Process ────┤ │
│ └── lineItem 1 ──► Process ────┘ │
└──────────────────────────── LoopResult ────────────────────────┘
- Each For-Each node is automatically matched with its corresponding End For-Each using depth-tracked BFS traversal.
- Each loop level creates an isolated execution context; inner loop state does not interfere with the outer loop.
- The generic
$item/$indexvariables always refer to the innermost loop. - Named variables (
$order,$orderIndex) persist across nesting levels, so inner loops can access outer loop items by name.
Named Variables for Nested Loops
When nesting loops, use Item Variable Name on each loop to keep variables accessible:
Outer loop (itemName = "order"):
$order = current order
$orderIndex = current order index (0-based)
Inner loop (itemName = "lineItem"):
$lineItem = current line item
$lineItemIndex = current line item index (0-based)
$order = still accessible (outer loop's current order)
$orderIndex = still accessible (outer loop's current index)
$item = same as $lineItem (innermost loop)
$index = same as $lineItemIndex (innermost loop)
If nesting exceeds 10 levels, a validation warning is generated and the pipeline is automatically disabled. In practice, deeply nested loops are rare, so consider flattening the data structure first using a Code or Set node if you approach this limit.
Watching a Loop Run
A loop is the one place where what you see live and what you see in Execution History legitimately differ, because a large loop produces far more per-node events than a browser tab should receive.
Live Execution samples iterations
While a run is in progress (Test Pipeline, or a manual run with the designer open), the engine streams node events to the browser. Sending every iteration of a 20,000-item loop would mean 40,000 frames for one node, so for nodes inside a For-Each the engine samples:
- the first 5 iterations of each body node are sent in full (started and completed, with output);
- after that, a progress frame every 100 ms carries the node's cumulative counts (started, completed, failed) and the latest iteration's output;
- the last iteration is sent in full when the loop finishes, so the node ends in its real terminal state.
On the canvas, the node tile's 20000x is the cumulative count from those progress frames. In the node inspector, the iteration navigator pages through the captures the browser actually received (the first few, one per progress frame, and the last) and names each by its real iteration number (for example Iteration 14001 of 20000). When the captures are fewer than the count, the inspector says so: Live view keeps 9 of 20000 iterations. Typing an iteration number jumps to the nearest capture.
Execution History keeps every iteration
Once the run is saved, open it from Execution History (or View on Canvas) and the navigator pages through every iteration. How much of each is kept depends on the pipeline's Execution History Detail (Advanced Settings):
| Execution History Detail | Per-iteration data kept |
|---|---|
| Full | index, status, duration, error, item and output |
| Standard | index, status, duration, error, with no item or output |
| Minimal | no per-node results, so no iterations |
If you need to inspect what each item produced after the fact, set Execution History Detail to Full. Full keeps every iteration's item and output in the execution record, so a 20,000-item loop over large rows makes a large record.
Restrictions and Validations
Stateful Nodes Inside Loops
Buffer and Aggregator nodes inside a For-Each loop body generate a validation warning when their blocking behavior is enabled:
| Node | Type | Condition | Warning |
|---|---|---|---|
| Buffer | transform.buffer | waitForFlush is true | Buffer node must have "Wait For Flush" disabled inside a loop. |
| Aggregator | transform.aggregator | waitForWindow is true | Aggregator node must have "Wait For Window" disabled inside a loop. |
These nodes are allowed inside loops when their blocking parameters are disabled (set to false or left at default), which enables continuous flow without blocking the loop.
When the blocking behavior is enabled, the pipeline is automatically disabled on save. You must either disable the blocking parameter or move the node outside the loop body to re-enable the pipeline.
For-Each Must Have a Matching End For-Each
Every For-Each node requires a corresponding End For-Each node connected downstream. If the engine cannot find a matching End For-Each at runtime (via graph traversal), the For-Each node fails with an error. The End For-Each is auto-created when you add a For-Each node, so this only occurs if you manually delete the End For-Each or disconnect it entirely.
Disconnected Loop Body Nodes
If a node is connected to the For-Each node but has no path to the End For-Each node, it is flagged as a warning (not an error). These nodes will not execute during the loop; they are effectively orphaned within the loop body. The pipeline editor shows a visual warning for these nodes.
Source Array Is Required
The Source Array expression is the only required parameter. Saving the pipeline with an empty Source Array produces a validation error on the For-Each node.
Usage Examples
Example 1: Process Each User From an API
A REST node named "Get Users" returns:
{
"users": [
{ "id": "U-1", "name": "Alice", "email": "alice@example.com" },
{ "id": "U-2", "name": "Bob", "email": "bob@example.com" }
]
}
For-Each configuration:
| Field | Value |
|---|---|
| Source Array | {{ $node["Get Users"].result.users }} |
| Error Mode | Stop on First Error |
| Result Mode | No Results |
Inside the loop body, use a Set or Code node to access each user:
{{ $item.name }} → "Alice" (iteration 0), "Bob" (iteration 1)
{{ $item.email }} → "alice@example.com", "bob@example.com"
{{ $index }} → 0, 1
This is a fire-and-forget pattern: each user is processed independently and no results are collected.
Example 2: Transform Each Item With a JavaScript Node
An upstream node returns sensor readings:
{
"readings": [
{ "sensorId": "S-1", "value": 10.2, "unit": "°C" },
{ "sensorId": "S-2", "value": 11.5, "unit": "°C" },
{ "sensorId": "S-3", "value": 9.8, "unit": "°C" }
]
}
Pipeline structure:
[Read Sensors] ──► [For-Each] ──► [Convert Temps] ──► [End For-Each] ──► [Send Report]
For-Each configuration:
| Field | Value |
|---|---|
| Source Array | {{ $node["Read Sensors"].result.readings }} |
| Item Variable Name | reading |
| Error Mode | Continue on Error |
| Result Mode | Collect All Results |
| Max Error Count | 0 (unlimited) |
| Partial Results | On |
Inside the loop body, a JavaScript node named "Convert Temps" transforms each reading. The code receives $reading (the named loop variable) and returns a new object:
const fahrenheit = $reading.value * 9/5 + 32;
return {
sensorId: $reading.sensorId,
celsius: $reading.value,
fahrenheit: Math.round(fahrenheit * 100) / 100
};
Because Result Mode is set to Collect All Results, every iteration's output from the End For-Each node is collected into the results array. After the loop, a downstream node accesses the converted data:
{{ $node["End For-Each"].result }}
→ [
{
"_loop": { "index": 0, "total": 3 },
"Convert Temps": { "sensorId": "S-1", "celsius": 10.2, "fahrenheit": 50.36 }
},
{
"_loop": { "index": 1, "total": 3 },
"Convert Temps": { "sensorId": "S-2", "celsius": 11.5, "fahrenheit": 52.70 }
},
{
"_loop": { "index": 2, "total": 3 },
"Convert Temps": { "sensorId": "S-3", "celsius": 9.8, "fahrenheit": 49.64 }
}
]
To access just the converted values from the "Convert Temps" node:
{{ $node["End For-Each"].result[0]["Convert Temps"].result.fahrenheit }} → 50.36
Example 3: Resilient Batch Processing with Retry
An order management system returns work orders to process:
{
"orders": [
{ "id": "WO-101", "type": "maintenance" },
{ "id": "WO-102", "type": "inspection" },
{ "id": "WO-103", "type": "maintenance" },
{ "id": "WO-104", "type": "repair" }
]
}
For-Each configuration:
| Field | Value |
|---|---|
| Source Array | {{ $node["Get Orders"].result.orders }} |
| Item Variable Name | order |
| Error Mode | Continue with Retry |
| Result Mode | Errors Only |
| Max Error Count | 5 |
| Error Threshold (%) | 50 |
| Partial Results | On |
Inside the loop body, each order is sent to an external API. If an API call fails due to a transient error (timeout, rate limit), the loop retries it with exponential backoff (up to 3 attempts by default). If all retries also fail, the error is recorded and the loop moves to the next order. The loop stops early if more than 5 errors occur or if more than 50% of processed items have failed.
After the loop, the output contains:
{{ $node["End For-Each"]._metadata.errorCount }} → number of failed orders
{{ $node["End For-Each"]._metadata.errors }} → details of each failure
{{ $node["End For-Each"]._metadata.partial }} → true if loop stopped early
Example 4: Filter Before Iteration
You can use expression functions to filter the source array before iterating:
For-Each configuration:
| Field | Value |
|---|---|
| Source Array | {{ filter($node["Get Orders"].result.orders, #.status == "active") }} |
| Error Mode | Stop on First Error |
| Result Mode | No Results |
Only orders with status == "active" are iterated; closed or pending orders are excluded entirely.
Example 5: Dynamic-Key Objects (e.g., Connector Read Results)
Some upstream nodes (particularly industrial connectors like Modbus, OPC UA, or S7) return arrays where each item is an object with a single dynamic key (the tag or register name) rather than a fixed property like name or id:
[
{"Energy_KWh": {"success": true, "value": 124299, "timestamp": "2026-02-16T08:04:53Z"}},
{"Coolant_Temp": {"success": true, "value": 218, "timestamp": "2026-02-16T08:04:53Z"}},
{"Spindle_Speed": {"success": true, "value": 0, "timestamp": "2026-02-16T08:04:53Z"}}
]
Here, $item.name will not work because there is no name property; the tag name is the object key. Use the keys() and values() built-in functions to extract them.
For-Each configuration:
| Field | Value |
|---|---|
| Source Array | {{ $node["Read Modbus"].result }} |
| Item Variable Name | sensorItem |
| Error Mode | Continue on Error |
| Result Mode | No Results |
Extracting the key and value inside the loop body:
{{ keys($sensorItem)[0] }} → "Energy_KWh", "Coolant_Temp", "Spindle_Speed"
{{ values($sensorItem)[0] }} → the full read-result object for each tag
{{ values($sensorItem)[0].value }} → 124299, 218, 0
{{ values($sensorItem)[0].success }} → true, true, true
Using the tag name in a dynamic UNS topic (in a UNS Publish node inside the loop):
| Field | Expression |
|---|---|
| Topic | enterprise/site1/area1/{{ keys($sensorItem)[0] }} |
| Value | {{ values($sensorItem)[0].value }} |
This publishes each tag to its own UNS topic: enterprise/site1/area1/Energy_KWh, enterprise/site1/area1/Coolant_Temp, etc.
keys() returns keys sorted alphabetically. When each object has only one key (as in the example above), this has no effect. For multi-key objects, use keys(obj)[0] with caution and consider restructuring the data first.
Example 6: Post-Loop Aggregation (Sum, Average, Metrics)
A common pattern is to use For-Each to transform each item, collect all results, and then compute aggregate metrics (totals, averages, counts) in a downstream node after the loop.
Scenario: A production line has multiple stations. An upstream node named "Get Stations" returns:
{
"stations": [
{ "id": "Station-1", "partsProduced": 150, "partsGood": 145, "partsBad": 5, "energyKWh": 45.2 },
{ "id": "Station-2", "partsProduced": 200, "partsGood": 198, "partsBad": 2, "energyKWh": 62.1 },
{ "id": "Station-3", "partsProduced": 180, "partsGood": 170, "partsBad": 10, "energyKWh": 53.8 },
{ "id": "Station-4", "partsProduced": 220, "partsGood": 215, "partsBad": 5, "energyKWh": 68.4 }
]
}
Pipeline structure:
[Get Stations] ──► [For-Each] ──► [Compute Quality] ──► [End For-Each] ──► [Aggregate] ──► [Publish Report]
Step 1 (For-Each configuration):
| Field | Value |
|---|---|
| Source Array | {{ $node["Get Stations"].result.stations }} |
| Item Variable Name | station |
| Error Mode | Continue on Error |
| Result Mode | Collect All Results |
Step 2 (inside the loop): JavaScript node "Compute Quality"
This node runs once per station. It calculates the quality rate and energy efficiency for each station:
const qualityRate = ($station.partsGood / $station.partsProduced) * 100;
const energyPerPart = $station.energyKWh / $station.partsProduced;
return {
id: $station.id,
partsProduced: $station.partsProduced,
partsGood: $station.partsGood,
partsBad: $station.partsBad,
qualityRate: Math.round(qualityRate * 100) / 100,
energyKWh: $station.energyKWh,
energyPerPart: Math.round(energyPerPart * 10000) / 10000
};
After all 4 iterations, the End For-Each output results array contains:
[
{
"_loop": { "index": 0, "total": 4 },
"Compute Quality": { "id": "Station-1", "partsProduced": 150, "partsGood": 145, "partsBad": 5, "qualityRate": 96.67, "energyKWh": 45.2, "energyPerPart": 0.3013 }
},
{
"_loop": { "index": 1, "total": 4 },
"Compute Quality": { "id": "Station-2", "partsProduced": 200, "partsGood": 198, "partsBad": 2, "qualityRate": 99.00, "energyKWh": 62.1, "energyPerPart": 0.3105 }
},
{
"_loop": { "index": 2, "total": 4 },
"Compute Quality": { "id": "Station-3", "partsProduced": 180, "partsGood": 170, "partsBad": 10, "qualityRate": 94.44, "energyKWh": 53.8, "energyPerPart": 0.2989 }
},
{
"_loop": { "index": 3, "total": 4 },
"Compute Quality": { "id": "Station-4", "partsProduced": 220, "partsGood": 215, "partsBad": 5, "qualityRate": 97.73, "energyKWh": 68.4, "energyPerPart": 0.3109 }
}
]
Step 3 (after the loop): Set node "Aggregate"
A Set node computes summary metrics using expression functions on the collected results. Since each result entry is keyed by the body node name, use pluck on the nested field:
| Field | Expression | Result |
|---|---|---|
| totalPartsProduced | {{ sum(pluck(pluck(pluck($node["End For-Each"].result, "Compute Quality"), "result"), "partsProduced")) }} | 750 |
| totalPartsGood | {{ sum(pluck(pluck(pluck($node["End For-Each"].result, "Compute Quality"), "result"), "partsGood")) }} | 728 |
| totalPartsBad | {{ sum(pluck(pluck(pluck($node["End For-Each"].result, "Compute Quality"), "result"), "partsBad")) }} | 22 |
| avgQualityRate | {{ round(avg(pluck(pluck(pluck($node["End For-Each"].result, "Compute Quality"), "result"), "qualityRate")), 2) }} | 96.96 |
| totalEnergyKWh | {{ round(sum(pluck(pluck(pluck($node["End For-Each"].result, "Compute Quality"), "result"), "energyKWh")), 2) }} | 229.5 |
| stationCount | {{ len($node["End For-Each"].result) }} | 4 |
The Aggregate node output is a single summary object:
{
"totalPartsProduced": 750,
"totalPartsGood": 728,
"totalPartsBad": 22,
"avgQualityRate": 96.96,
"totalEnergyKWh": 229.5,
"stationCount": 4
}
Key expression patterns used:
| Pattern | What It Does |
|---|---|
pluck(arr, "field") | Extracts a single field from every object in the array, producing a flat array of values. |
pluck(pluck(arr, "NodeName"), "field") | Nested pluck: first extracts each node's output from the result entries, then extracts a field from each. |
sum(pluck(...)) | Combines pluck with sum to total a specific field across all results. |
avg(pluck(...)) | Average of a specific field. |
round(value, decimals) | Rounds the result to avoid floating-point noise. |
filter(arr, #.field < threshold) | Filters results that match a condition. |
count(arr, #.field < threshold) | Counts how many results match a condition. |
Use Collect All Results + post-loop aggregation when you need summary statistics from the loop. This is the equivalent of a SQL GROUP BY; the loop body transforms/enriches each row, and the downstream node computes the rollup. For very large arrays (10,000+ items), consider using a JavaScript node after the loop instead of many individual Set expressions, to avoid repeated iteration over the results array.
Example 7: Nested Loops: Orders with Line Items
A common use case is iterating over orders, where each order contains a list of line items:
{
"orders": [
{
"id": "ORD-1",
"customer": "Acme Corp",
"items": [
{ "sku": "A100", "qty": 5 },
{ "sku": "B200", "qty": 3 }
]
},
{
"id": "ORD-2",
"customer": "Globex",
"items": [
{ "sku": "C300", "qty": 10 }
]
}
]
}
Pipeline structure:
[Get Orders] ──► [Outer For-Each] ──► [Inner For-Each] ──► [Process Item] ──► [Inner End] ──► [Outer End] ──► [Summary]
Outer For-Each configuration:
| Field | Value |
|---|---|
| Source Array | {{ $node["Get Orders"].result.orders }} |
| Item Variable Name | order |
| Result Mode | Collect All Results |
Inner For-Each configuration:
| Field | Value |
|---|---|
| Source Array | {{ $order.items }} |
| Item Variable Name | lineItem |
| Result Mode | Collect All Results |
Inside the inner loop body, both variables are available:
{{ $order.id }} → "ORD-1" (outer loop)
{{ $order.customer }} → "Acme Corp" (outer loop)
{{ $lineItem.sku }} → "A100" (inner loop, iteration 0)
{{ $lineItem.qty }} → 5 (inner loop, iteration 0)
{{ $item }} → same as $lineItem (innermost loop)
{{ $index }} → 0 (innermost loop index)
{{ $orderIndex }} → 0 (outer loop index)
Pipeline Structure Diagram
The following diagram shows a typical For-Each pipeline structure:
Trigger ──► Get Data ──► For-Each ──► Process (per item) ──► End For-Each ──► Send Report
│ └── loop body ──────────┘ │
│ │
└──────── LoopResult (after all iterations) ────────┘
- Trigger → starts the pipeline.
- Get Data → fetches the array (e.g., from an API, database, or connector).
- For-Each → evaluates the Source Array expression and begins iteration.
- Process (per item) → any number of nodes between For-Each and End For-Each (the loop body). These execute once per item.
- End For-Each → marks the end of the loop body; collects each iteration's output.
- Send Report → runs once after all iterations complete, receiving the LoopResult.
When to Use For-Each Loop
Use the For-Each Loop node when:
- You receive arrays from connectors or upstream transforms and want each element to drive a separate execution of downstream logic.
- You need a clear, declarative way to pick which collection to iterate, without hand-rolling loops in Code nodes.
- You want iteration to integrate cleanly with other nodes like Data Instance, Set, and Code, using
$itemand$indexfor per-element access. - You need error resilience: continue processing remaining items even if some fail, with configurable retry and threshold logic.
- Always pair a For-Each node with an End For-Each node. The pipeline engine uses End For-Each to determine which nodes belong to the loop body.
- Use named variables (
itemName) for clarity, especially when the generic$itemcould be ambiguous or when using nested loops. - Choose the right Result Mode: avoid
allfor large arrays unless downstream nodes actually need every result. - Set error thresholds when using Continue or Collect All modes to prevent runaway failures from processing thousands of items.
- Access results from End For-Each: the aggregated LoopResult is on the End For-Each node (e.g.,
$node["End For-Each"].result), not the For-Each node.
Configuration reference
The fields below are generated from the node's config contract, so they match what the pipeline validator enforces and what the designer's form offers.
functions.loop.end
| Field | Type | Required | Default | Values | Description |
|---|---|---|---|---|---|
loopId | string | no | - | - | Optional loop identifier that pairs this End For-Each with the For-Each carrying the same loopId, for hand-built nested loops. Leave empty; the usual case and what the designer always does; to close the For-Each at the same nesting level. This node has nothing else to configure |
functions.loop.foreach
| Field | Type | Required | Default | Values | Description |
|---|---|---|---|---|---|
sourceArray | any | yes | - | accepts an expression | Expression that resolves to the array or object to iterate, e.g. {{ $node["Data"].result }}; an array yields $item and $index per iteration, an object yields $item (value) and $key in key order. A literal JSON array or object is also accepted |
itemName | string | no | - | - | Optional name for the current item: with itemName "order" the body can use $order and $orderIndex as well as $item and $index. Must be an identifier (letter or underscore first) and must not be a built-in variable name such as item, index, key, input, trigger, node or execution |
loopId | string | no | - | - | Optional loop identifier matched against the loopId of an End For-Each node. Leave empty to pair with the End For-Each at the same nesting level, which is the usual case |
errorMode | string | no | stopOnFirst | stopOnFirst, continue, continueWithRetry, collectAll | What to do when an iteration fails: stopOnFirst halts the loop and fails the node; continue skips the item and records the error; continueWithRetry retries the item with backoff (see itemRetryConfig) before skipping it; collectAll processes every item and reports all errors at the end |
resultMode | string | no | none | none, errorsOnly, last, all | What the End For-Each collects: none keeps only counts (fire-and-forget); errorsOnly keeps only the failed items; last keeps the final successful result; all keeps every iteration's result in order (memory grows with the item count) |
maxErrorCount | integer | no | 0 | 0 to 1000 | Stop the loop early once this many iterations have failed; 0 means unlimited. Only applies when errorMode is not stopOnFirst |
errorThresholdPercent | number | no | 0 | 0 to 100 | Stop the loop early when the percentage of failed iterations exceeds this value (50 = stop once more than half have failed); 0 disables the check. Only applies when errorMode is not stopOnFirst |
partialResultsOnError | boolean | no | true | - | true emits the results collected so far when the loop stops early on errors; false emits no results in that case |
concurrency | integer | no | 1 | 1 to 32 | How many iterations may run at the same time, 1 to 32. 1 (the default) runs items one after another in order; above 1 iterations run in isolated scopes; body-node outputs are not visible outside the loop, use the End For-Each's collected results instead |
itemRetryConfig | object | no | - | - | Per-item retry policy used only when errorMode is continueWithRetry; omit it for the defaults (3 attempts, 1s initial delay doubling up to 2m, 10% jitter). Only transient errors are retried |