Loop Nodes
The workflow engine provides native loop node types — forEachLoop and whileLoop — for structured iteration over collections or conditions. These are container nodes that manage their own child nodes and loop state.
Gateway-Based Loops
For simpler iteration using gateways, see Loop Patterns. Native loop nodes are better for structured iteration with automatic state management.
ForEach Loop
Iterates over a collection, executing child nodes once per item.
Definition
final loop = ContainerWorkflowNode(
id: 'processItems',
type: NodeType.forEachLoop,
name: 'Process Each Item',
config: ForEachLoopNodeConfiguration(
collectionPath: 'items',
loopId: 'itemLoop',
storeAs: 'processedItems',
),
nodeIds: ['validateItem', 'transformItem'], // Child nodes
);Or in JSON:
{
"id": "processItems",
"type": "forEachLoop",
"name": "Process Each Item",
"config": {
"schemaType": "config.forEachLoop",
"collectionPath": "items",
"loopId": "itemLoop",
"storeAs": "processedItems"
},
"nodeIds": ["validateItem", "transformItem"]
}ForEach Properties
| Property | Type | Description |
|---|---|---|
collectionPath | String | Path to the collection in accumulated output |
loopId | String | Unique identifier for this loop's state |
storeAs | String? | Key for storing loop results in output |
simulationMode | Strategy for simulation (optional) |
Output Ports
ForEach loops have two output ports:
| Port | ID | Description |
|---|---|---|
| Iteration | iteration | Taken for each item in the collection |
| Completed | completed | Taken when all items are processed |
Loop State
Inside child nodes, access the loop state via ExecutionContext:
class ProcessItemTask extends TaskExecutor {
@override
Future<TaskResult> execute(ExecutionContext context) async {
// Access the strongly-typed loop state
final loop = context.forEachLoop('itemLoop');
final item = loop.item; // Current item
final index = loop.index; // 0-based index
final total = loop.total; // Collection length
final isFirst = loop.isFirst; // First iteration?
final isLast = loop.isLast; // Last iteration?
final key = loop.key; // Key (for maps)
// Process the item
final result = await processItem(item);
return TaskSuccess([SetOutputEffect(output: {
'processed': true,
'itemId': item['id'],
'result': result,
})]);
}
@override
String get schemaType => 'task.processItem';
@override
String get name => 'Process Item';
}ForEachLoopState
| Property | Type | Description |
|---|---|---|
index | int | 0-based iteration index |
item | dynamic | Current collection item |
key | dynamic | Key for the current item (for maps) |
total | int | Total number of items |
isFirst | bool | Whether this is the first iteration |
isLast | bool | Whether this is the last iteration |
collection | dynamic | The full collection being iterated |
While Loop
Repeats child nodes while a condition evaluates to true.
Definition
final loop = ContainerWorkflowNode(
id: 'retryUntilSuccess',
type: NodeType.whileLoop,
name: 'Retry Until Success',
config: WhileLoopNodeConfiguration(
conditionPath: 'shouldRetry',
loopId: 'retryLoop',
storeAs: 'retryResult',
),
nodeIds: ['attemptTask', 'checkResult'],
);Or in JSON:
{
"id": "retryUntilSuccess",
"type": "whileLoop",
"name": "Retry Until Success",
"config": {
"schemaType": "config.whileLoop",
"conditionPath": "shouldRetry",
"loopId": "retryLoop",
"storeAs": "retryResult"
},
"nodeIds": ["attemptTask", "checkResult"]
}While Properties
| Property | Type | Description |
|---|---|---|
conditionPath | String | Path to a boolean in accumulated output |
loopId | String | Unique identifier for this loop's state |
storeAs | String? | Key for storing loop results in output |
simulationMode | Strategy for simulation (optional) |
Loop State
class RetryTask extends TaskExecutor {
@override
Future<TaskResult> execute(ExecutionContext context) async {
// Access the while-loop state
final loop = context.whileLoop('retryLoop');
final iteration = loop.iteration; // 1-based iteration count
// Perform the attempt
final result = await attemptOperation();
return TaskSuccess([SetOutputEffect(output: {
'success': result.isSuccess,
'shouldRetry': !result.isSuccess && iteration < 5,
'attempt': iteration,
})]);
}
@override
String get schemaType => 'task.retry';
@override
String get name => 'Retry Task';
}WhileLoopState
| Property | Type | Description |
|---|---|---|
iteration | int | 1-based iteration count |
LoopContext Extension
The LoopContextExtension on ExecutionContext provides convenient accessors:
// Check if currently inside a loop
if (context.isInLoop('myLoop')) {
// Access forEach loop state
final forEachState = context.forEachLoop('myLoop');
// Or while loop state
final whileState = context.whileLoop('myLoop');
}Container Nodes
Loop nodes use ContainerWorkflowNode instead of LeafWorkflowNode. The key difference is the nodeIds property, which lists the IDs of child nodes that execute within the loop body:
// Regular node
final task = LeafWorkflowNode(
id: 'myTask',
type: NodeType.serviceTask,
name: 'My Task',
config: ...,
);
// Container node (for loops)
final loop = ContainerWorkflowNode(
id: 'myLoop',
type: NodeType.forEachLoop,
name: 'My Loop',
config: ...,
nodeIds: ['childTask1', 'childTask2'], // Child nodes
);The child nodes referenced by nodeIds are standard LeafWorkflowNode instances defined elsewhere in the workflow. The loop container manages their execution order and passes loop state through the execution context.
Use Cases
Batch Processing
Process a list of items with per-item error handling:
// ForEach loop over order items
final batchLoop = ContainerWorkflowNode(
id: 'processOrderItems',
type: NodeType.forEachLoop,
name: 'Process Order Items',
config: ForEachLoopNodeConfiguration(
collectionPath: 'order.items',
loopId: 'orderItemLoop',
storeAs: 'processedItems',
),
nodeIds: ['validateItem', 'calculatePrice', 'checkInventory'],
);Polling Until Ready
Poll an external system until a condition is met:
final pollLoop = ContainerWorkflowNode(
id: 'pollForStatus',
type: NodeType.whileLoop,
name: 'Poll for Status',
config: WhileLoopNodeConfiguration(
conditionPath: 'needsPolling',
loopId: 'pollLoop',
storeAs: 'pollResult',
),
nodeIds: ['checkStatus', 'waitInterval'],
);Multi-Reviewer Cycle
Iterate through a list of reviewers:
final reviewLoop = ContainerWorkflowNode(
id: 'reviewCycle',
type: NodeType.forEachLoop,
name: 'Review Cycle',
config: ForEachLoopNodeConfiguration(
collectionPath: 'reviewers',
loopId: 'reviewLoop',
storeAs: 'reviewResults',
),
nodeIds: ['assignReviewer', 'awaitReview', 'recordFeedback'],
);ForEach vs While vs Gateway Loops
| Aspect | ForEach Loop | While Loop | Gateway Loop |
|---|---|---|---|
| Use case | Iterate a collection | Repeat until condition | Manual loop control |
| State management | Automatic (ForEachLoopState) | Automatic (WhileLoopState) | Manual (task output) |
| Termination | All items processed | Condition becomes false | Gateway branch |
| Child nodes | nodeIds in container | nodeIds in container | Standard edges |
| Builder support | JSON / direct construction | JSON / direct construction | WorkflowBuilder |
| Best for | Known collections | Polling, retry | Simple counted loops |
Best Practices
- Use
forEachLoopfor collections - Automatic item tracking and index management - Use
whileLoopfor conditions - Polling, retry-until-success patterns - Use gateway loops for simple cases - When you don't need structured loop state
- Always set
loopId- Required for accessing loop state in child tasks - Use
storeAs- Namespace loop results in the workflow output - Guard against infinite while loops - Include an iteration limit in the condition logic
- Access loop state via extensions - Use
context.forEachLoop(id)andcontext.whileLoop(id)
Next Steps
- Loop Patterns - Gateway-based loop patterns
- Subflow - Subprocess invocation
- Gateways - Routing and branching
- Data Flow - How data moves through loops