Skip to main content
Version: 2.2-dev
For-Each Loop node interface

For-Each Loop node

For-Each Loop Node

Overview

The For-Each Loop turns a single array payload into multiple executions of downstream nodes — one run per item. Instead of building manual loops or complex branching, you define a single array 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.

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) ────┘
  1. The For-Each node evaluates the Source Array expression and determines the iteration count.
  2. 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.
  3. 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.
  4. 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.
Auto-Pairing

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.


Loop Variables

Inside the loop body, the following expression variables are available:

VariableTypeDescription
$itemanyThe current array element being processed (always refers to the innermost loop).
$indexnumberZero-based index of the current iteration (0, 1, 2, …) for the innermost loop.

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:

VariableTypeDescription
$orderanyAlias for the current item — identical to $item but named for clarity.
$orderIndexnumberAlias 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)
{{ $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

For-Each configuration panel

Source Array (required)

TypeExpression (string)
Default"" (empty)
Placeholder{{ $node["Data"].items }}

The expression that resolves to the array to iterate over. This is the only required parameter. If the expression does not resolve to an array, the node will error.

Expression examples:

ExpressionDescription
{{ $node["API"].data }}Array from an API response node
{{ $node["Query"].items }}Extract the items field from a query result
{{ $node["Data"].records }}Array from a data node
{{ $trigger.payload }}Array from the trigger's payload directly
{{ filter($node["Orders"].list, #.status == "active") }}Filtered subset using a built-in function

Item Variable Name (optional)

TypeText (alphanumeric + underscore only)
Default"" (empty — uses $item / $index only)
Max Length30 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

TypeSelect
DefaultstopOnFirst

Controls how the loop handles failures during iteration.

ValueLabelBehavior
stopOnFirstStop on First ErrorStops the loop immediately when any iteration fails. The entire For-Each node is marked as failed. This is the default and safest option.
continueContinue on ErrorSkips the failed item and continues with the remaining items. Failed items are recorded in the output's errors array.
continueWithRetryContinue with RetryRetries 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.
collectAllCollect All ErrorsProcesses 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:

ParameterTypeDefaultRangeDescription
Max Error Countnumber00–1,000Maximum number of errors before stopping the loop early. 0 means unlimited — the loop will process all items.
Error Threshold (%)number00–100Stop 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 Resultstoggletrue (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.
note

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

TypeSelect
Defaultnone

Controls what iteration results are collected in the output.

ValueLabelMemoryDescription
noneNo ResultsO(1)Fire-and-forget — only tracks count, successCount, and errorCount. Best for side-effect loops (e.g., sending notifications). This is the default.
errorsOnlyErrors OnlyO(errors)Only tracks failed items in the errors array. Useful for debugging without the overhead of collecting all results.
lastKeep Last OnlyO(1)Only keeps the last successful iteration's result. Useful when only the final computation matters.
allCollect All ResultsO(n)Stores every iteration's result in the results array. Required when downstream nodes need the full set of processed data.
Memory usage

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:

SettingDefaultDescription
Max Attempts3Maximum retry attempts per failed item.
Initial Delay1,000 msWait time before the first retry.
Max Delay120,000 ms (2 min)Upper bound for the backoff delay.
Multiplier2.0Exponential backoff multiplier. Each retry waits initialDelay × multiplier^attempt.
Jitter Factor0.1Adds ±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:

SettingOptionsDefaultDescription
Timeout (seconds)numberPipeline defaultMaximum execution time for the node (1–600).
Retry on TimeoutPipeline Default / Enabled / DisabledPipeline DefaultWhether to retry the node-level execution on timeout.
Retry on FailPipeline Default / Enabled / DisabledPipeline DefaultWhether to retry the node-level execution on failure. When Enabled, shows Advanced Retry Configuration.
On ErrorPipeline Default / Stop Pipeline / Continue ExecutionPipeline DefaultBehavior when the node fails after all retries.
note

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.


End For-Each Node

End For-Each node interface

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 _loop metadata.
  • 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

  1. Add a For-Each node to start the loop (this auto-creates the End For-Each).
  2. Place your processing nodes between For-Each and End For-Each.
  3. Connect the last processing node to End For-Each.
  4. 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"].results }}.

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
}
note

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

FieldTypePresentDescription
resultsarrayAlwaysIteration 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: empty [] for none and errorsOnly, single item for last, all items for all.
countnumberAlwaysTotal number of items in the source array (attempted iterations).
successCountnumberAlwaysNumber of iterations that completed successfully.
errorCountnumberAlwaysNumber of iterations that failed.
partialbooleanAlwaystrue if the loop stopped early (due to error thresholds or max error count) before processing all items.
errorsarrayOnly when errors occurDetails 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.
stopReasonstringOnly on early stopReason 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"].results }}                    — full results array
{{ $node["End For-Each"].results[0]["Process"].name }} — first result's "Process" node output
{{ $node["End For-Each"].results[0]._loop }} — loop metadata of first result
{{ $node["End For-Each"].count }} — total items attempted
{{ $node["End For-Each"].successCount }} — successful iterations
{{ $node["End For-Each"].errorCount }} — failed iterations
{{ $node["End For-Each"].partial }} — whether results are incomplete
{{ $node["End For-Each"].errors }} — error details (only if errors occurred)
{{ $node["End For-Each"].stopReason }} — early stop reason (only if stopped early)
tip

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 / $index variables 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)
Nesting depth limit

If nesting exceeds 10 levels, a validation warning is generated and the pipeline is automatically disabled. In practice, deeply nested loops are rare — consider flattening the data structure first using a Code or Set node if you approach this limit.


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:

NodeTypeConditionWarning
Buffertransform.bufferwaitForFlush is trueBuffer node must have "Wait For Flush" disabled inside a loop.
Aggregatortransform.aggregatorwaitForWindow is trueAggregator 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.

warning

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:

FieldValue
Source Array{{ $node["Get Users"].users }}
Error ModeStop on First Error
Result ModeNo 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:

FieldValue
Source Array{{ $node["Read Sensors"].readings }}
Item Variable Namereading
Error ModeContinue on Error
Result ModeCollect All Results
Max Error Count0 (unlimited)
Partial ResultsOn

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"].results }}
→ [
{
"_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"].results[0]["Convert Temps"].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:

FieldValue
Source Array{{ $node["Get Orders"].orders }}
Item Variable Nameorder
Error ModeContinue with Retry
Result ModeErrors Only
Max Error Count5
Error Threshold (%)50
Partial ResultsOn

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"].errorCount }}    → number of failed orders
{{ $node["End For-Each"].errors }} → details of each failure
{{ $node["End For-Each"].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:

FieldValue
Source Array{{ filter($node["Get Orders"].orders, #.status == "active") }}
Error ModeStop on First Error
Result ModeNo 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:

FieldValue
Source Array{{ $node["Read Modbus"].data }}
Item Variable NamesensorItem
Error ModeContinue on Error
Result ModeNo 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):

FieldExpression
Topicenterprise/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.

note

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:

FieldValue
Source Array{{ $node["Get Stations"].stations }}
Item Variable Namestation
Error ModeContinue on Error
Result ModeCollect 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:

FieldExpressionResult
totalPartsProduced{{ sum(pluck(pluck($node["End For-Each"].results, "Compute Quality"), "partsProduced")) }}750
totalPartsGood{{ sum(pluck(pluck($node["End For-Each"].results, "Compute Quality"), "partsGood")) }}728
totalPartsBad{{ sum(pluck(pluck($node["End For-Each"].results, "Compute Quality"), "partsBad")) }}22
avgQualityRate{{ round(avg(pluck(pluck($node["End For-Each"].results, "Compute Quality"), "qualityRate")), 2) }}96.96
totalEnergyKWh{{ round(sum(pluck(pluck($node["End For-Each"].results, "Compute Quality"), "energyKWh")), 2) }}229.5
stationCount{{ len($node["End For-Each"].results) }}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:

PatternWhat 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.
When to use this pattern

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:

FieldValue
Source Array{{ $node["Get Orders"].orders }}
Item Variable Nameorder
Result ModeCollect All Results

Inner For-Each configuration:

FieldValue
Source Array{{ $order.items }}
Item Variable NamelineItem
Result ModeCollect 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 $item and $index for per-element access.
  • You need error resilience — continue processing remaining items even if some fail, with configurable retry and threshold logic.
Best practices
  • 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 $item could be ambiguous or when using nested loops.
  • Choose the right Result Mode — avoid all for 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"].results), not the For-Each node.