Skip to main content

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

ParameterTypeDefaultDescription
dataObject/Array-The data to process — can be an array or an object containing an array
iterableFieldString'list'The field name to extract the array from the data object
maxIterationsNumber1000Maximum iteration limit to prevent infinite loops
continueOnErrorBooleantrueWhether to continue processing other items when a single item fails

Output Ports

Port NameDescription
itemConnects 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:

  1. If data is an array → iterate directly
  2. If data is an object → extract the iterableField field (defaults to list)
  3. 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:

  1. 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.
  2. The custom-code node receives a read-only context snapshot. The js-executor operator deep-clones the context into a read-only snapshot before running your code, so any assignment to context.* 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
}
Important Note

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.