ForEach Control Node
Overview
The ForEach control node is used to iterate over each element in an array, creating an independent sub-workflow environment for processing each element. As a pure control node, it focuses on loop control logic and does not perform result aggregation.
Node Configuration
Basic Properties
{
"id": "foreach-1",
"type": "control",
"config": {
"identifier": "foreach",
"title": "Array Loop Processing",
"description": "Process array data item by item"
}
}
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
data | Object/Array | - | The data to process — can be an array or an object containing an array |
iterableField | String | 'list' | The field name to extract the array from the data object |
maxIterations | Number | 1000 | Maximum iteration limit to prevent infinite loops |
continueOnError | Boolean | true | Whether to continue processing other items when a single item fails |
Output Ports
| Port Name | Description |
|---|---|
item | Connects to the sub-workflow, passing the current iteration item |
Data Input Methods
Method 1: Direct Array
When data itself is an array, ForEach iterates over it directly:
// Data generator returns
return [1, 2, 3, 4, 5];
// ForEach will iterate over each number
Method 2: Array Field Within an Object
When data is an object, ForEach extracts the array from the specified field:
// Data generator returns
return {
list: [
{ id: 1, name: "Product A" },
{ id: 2, name: "Product B" }
],
total: 2
};
// ForEach will iterate over the array in the list field
Method 3: Custom Field Name
Specify the field name using the iterableField parameter:
// Data generator returns
return {
products: [
{ id: 1, name: "Product A" },
{ id: 2, name: "Product B" }
]
};
// Set iterableField to "products"
Data retrieval logic:
- If
datais an array → iterate directly - If
datais an object → extract theiterableFieldfield (defaults tolist) - Otherwise → use an empty array
Iteration Context
ForEach provides rich context information for each iteration, accessible within the sub-workflow via context[nodeId]:
// Context structure
context['foreach-1'] = {
item: currentItem, // Current iteration item
_iteration_info: {
index: 0, // Current index (zero-based)
total: 5, // Total number of items
isFirst: true, // Whether this is the first item
isLast: false // Whether this is the last item
}
}
Usage Examples
Example 1: Numeric Array Processing
{
"nodes": {
"data-generator": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": "return [10, 20, 30, 40, 50];"
}
},
"foreach-numbers": {
"type": "control",
"config": {
"identifier": "foreach"
}
},
"number-processor": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": `
const number = context['foreach-numbers'].item;
const info = context['foreach-numbers']._iteration_info;
console.log(\`Processing number \${info.index + 1}: \${number}\`);
return {
original: number,
doubled: number * 2,
isLast: info.isLast
};
`
}
}
},
"edges": [
{
"source": "data-generator:result",
"target": "foreach-numbers:data"
},
{
"source": "foreach-numbers:item",
"target": "number-processor:parameters"
}
]
}
Example 2: Object Array Processing
{
"nodes": {
"user-generator": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": `
return {
list: [
{ id: 1, name: "Alice", age: 25 },
{ id: 2, name: "Bob", age: 30 },
{ id: 3, name: "Charlie", age: 28 }
]
};
`
}
},
"foreach-users": {
"type": "control",
"config": {
"identifier": "foreach"
}
},
"user-processor": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": `
const user = context['foreach-users'].item;
const info = context['foreach-users']._iteration_info;
return {
...user,
processOrder: info.index + 1,
isAdult: user.age >= 18,
isFirstUser: info.isFirst,
isLastUser: info.isLast
};
`
}
}
},
"edges": [
{
"source": "user-generator:result",
"target": "foreach-users:data"
},
{
"source": "foreach-users:item",
"target": "user-processor:parameters"
}
]
}
Example 3: Aggregating Results After the Loop
Each iteration runs in its own context and cannot accumulate into a shared variable (see the note below). The correct pattern is: have the per-iteration node return a value for each item, then aggregate after the loop in a downstream node that runs once the loop has finished.
{
"nodes": {
"score-generator": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": "return [85, 92, 78, 96, 88];"
}
},
"foreach-scores": {
"type": "control",
"config": {
"identifier": "foreach"
}
},
"score-processor": {
"type": "operator",
"config": {
"operator": "jsexecutor",
"code": "const score = context['foreach-scores'].item; return { score: score, passed: score >= 60 };"
}
}
},
"edges": [
{
"source": "score-generator:result",
"target": "foreach-scores:data"
},
{
"source": "foreach-scores:item",
"target": "score-processor:parameters"
}
]
}
In this example each iteration of score-processor returns { score, passed } for one item. The ForEach node collects these per-node results across all iterations. To compute the average or other aggregates, add a node after the ForEach node that reads the collected results and reduces them — the loop itself does not return aggregated data.
Error Handling
Default Behavior (continueOnError = true)
// When a single item fails, continue processing other items
const continueOnError = inputs.continueOnError !== false; // Defaults to true
if (continueOnError) {
console.warn('Iteration failed, continuing to next iteration');
results.push(null); // Failed iterations are represented as null
}
Strict Mode (continueOnError = false)
// Any failure stops the entire loop
if (!continueOnError) {
throw error; // Throw exception, stop execution
}
Execution Characteristics
Sequential Execution
ForEach uses sequential execution mode, processing array elements one by one:
- Waits for the current iteration to complete before starting the next
- Avoids concurrent resource contention
- Guarantees processing order
Execution Metrics
ForEach outputs detailed execution statistics:
// Console output example
ForEach node execution completed {
nodeId: 'foreach-scores',
totalIterations: 5,
successCount: 5,
errorCount: 0,
executionTime: '125ms'
}
Safety Limits
- Maximum iterations: Defaults to 1000 to prevent infinite loops
- Data conversion: Automatically handles non-array data
null/undefined→ empty array[]- Object →
Object.values(object) - Other types →
[value]
Why You Cannot Accumulate Across Iterations
A common mistake is to try to build up a running total or collect results by writing to a shared variable on the context (for example context._sum += item) and reading it back on the last iteration. This does not work, for two reasons rooted in how the engine runs each iteration:
- Each iteration gets a fresh context copy. ForEach creates a new context object (
{...context}) for every iteration, so a value you set in one iteration is not visible in the next. - The custom-code node receives a read-only context snapshot. The
js-executoroperator deep-clones the context into a read-only snapshot before running your code, so any assignment tocontext.*inside the code is discarded.
As a result, patterns that mutate context._sum, context._results, context._batch, etc. and read them back on isLast silently lose their data. This is also why the warning below states that ForEach itself returns no aggregated result.
The Correct Pattern: Aggregate Downstream
Do the per-item work inside the loop, returning a value for each item, and perform any accumulation, filtering, or batching in a node placed after the ForEach node:
data source → ForEach (item) → per-item processor
│
└── (loop completes) → aggregator node
The per-item processor returns one result per item; the aggregator runs once the loop finishes and reduces the collected results into a total, a filtered list, batches, or whatever you need. Keep the in-loop code stateless — rely only on item and _iteration_info for the current iteration.
Best Practices
1. Data Input Recommendations
// ✅ Recommended: Direct array (simplest)
return [1, 2, 3, 4, 5];
// ✅ Recommended: Structured object (more flexible)
return {
list: [1, 2, 3, 4, 5],
metadata: { source: 'api', timestamp: Date.now() }
};
2. Context Usage Tips
Keep the in-loop code stateless — read only item and _iteration_info for the current iteration, and return your per-item result. Do not write to context.* to carry data between iterations; those writes are discarded (see Why You Cannot Accumulate Across Iterations).
// Leverage iteration info (read-only) and return a per-item result
const processor = `
const item = context['foreach-1'].item;
const info = context['foreach-1']._iteration_info;
return {
item: item,
position: info.index + 1,
isFirst: info.isFirst,
isLast: info.isLast
};
`;
3. Error Handling Strategy
// Choose error handling mode based on requirements
{
"continueOnError": true // Tolerant mode, suitable for data cleansing
}
{
"continueOnError": false // Strict mode, suitable for critical business logic
}
ForEach is a pure control node and does not return aggregated results. All business logic and data aggregation should be performed within the nodes of the sub-workflow.
Related Documentation
- Workflow Overview — Workflow engine fundamentals
- Context Management — Workflow context and data passing
- If-Else Condition Node — Conditional control node
- Switch Branch Node — Multi-branch control node