Loop Patterns
Patterns for iterative workflow execution.
Native Loop Nodes
The engine also provides native forEachLoop and whileLoop node types with automatic state management. See Loop Nodes for structured iteration without manual gateway wiring.
Gateway-First Loop Pattern
A simple approach for manual loop control: a gateway controls loop continuation.
Example: Process Items in a List
dart
final workflow = WorkflowBuilder(
id: 'process-items',
code: 'ITEMS',
name: 'Process Items Loop',
)
.start('begin')
// Setup: Initialize loop variables
.task('setup', execute: (ctx) async {
// Use getInitial<T> for original workflow input
final items = ctx.getInitialRequired<List>('items');
return {
'items': items,
'currentIndex': 0,
'totalItems': items.length,
'hasMore': items.isNotEmpty,
};
})
// Gateway controls loop entry
.oneOf('checkHasMore', [
Branch.whenFn((o) => o['hasMore'] == true, then: 'processItem'),
Branch.otherwise(then: 'complete'),
])
// Process current item
.task('processItem', execute: (ctx) async {
// Use getAny<T> for accumulated output
final items = ctx.getAny<List>('items')!;
final index = ctx.getAny<int>('currentIndex')!;
final item = items[index];
// Do work with item
await processItem(item);
return {'processedItem': item};
})
// Increment and check for more
.task('increment', execute: (ctx) async {
// Use getAny<T> for accumulated output
final index = ctx.getAny<int>('currentIndex')!;
final total = ctx.getAny<int>('totalItems')!;
final nextIndex = index + 1;
return {
'currentIndex': nextIndex,
'hasMore': nextIndex < total,
};
})
.end('complete')
// Flow edges
.connect('begin', 'setup')
.connect('setup', 'checkHasMore')
.connect('processItem', 'increment')
.connect('increment', 'checkHasMore') // Loop back to gateway
.build();Multi-Level Approval Loop
A common pattern for approval chains:
dart
final workflow = WorkflowBuilder(
id: 'multi-level-approval',
code: 'MLA',
name: 'Multi-Level Approval',
)
.start('begin')
// Build approval chain
.task('buildChain', execute: (ctx) async {
// Use getInitial<T> for original workflow input
final entityId = ctx.getInitialRequired<String>('entityId');
final chain = await approvalService.buildChain(entityId);
return {
'approvalChain': chain,
'currentLevel': 0,
'totalLevels': chain.length,
'hasMoreLevels': chain.isNotEmpty,
};
})
// Loop control gateway
.oneOf('checkHasMoreLevels', [
Branch.whenFn((o) => o['hasMoreLevels'] == true, then: 'startLevel'),
Branch.otherwise(then: 'makeEffective'),
])
// Notify approver for current level
.task('startLevel', execute: (ctx) async {
// Use getAny<T> for accumulated output
final level = ctx.getAny<int>('currentLevel')!;
final chain = ctx.getAny<List>('approvalChain')!;
final approver = chain[level];
return {
'currentApprover': approver,
'levelStartedAt': DateTime.now().toIso8601String(),
};
})
// Wait for approval decision
.userTask('approvalDecision',
schemaType: 'approval',
title: 'Level {{currentLevel}} Approval',
storeAs: 'levelDecision',
)
// Route based on decision
.oneOf('routeDecision', [
Branch.whenFn(
(o) => o['levelDecision']?['decision'] == 'approved',
then: 'incrementLevel',
),
Branch.whenFn(
(o) => o['levelDecision']?['decision'] == 'rejected',
then: 'handleRejection',
),
Branch.otherwise(then: 'handleRejection'),
])
// Increment level for next iteration
.task('incrementLevel', execute: (ctx) async {
// Use getAny<T> for accumulated output
final current = ctx.getAny<int>('currentLevel')!;
final total = ctx.getAny<int>('totalLevels')!;
final nextLevel = current + 1;
return {
'currentLevel': nextLevel,
'hasMoreLevels': nextLevel < total,
};
})
// Final approval - make effective
.task('makeEffective', execute: (ctx) async {
// Use getAny<T> for accumulated output
final entityId = ctx.getAny<String>('entityId')!;
await entityService.makeEffective(entityId);
return {'effectiveAt': DateTime.now().toIso8601String()};
})
// Handle rejection
.task('handleRejection', execute: (ctx) async {
// Use getAny<T> for accumulated output
final entityId = ctx.getAny<String>('entityId')!;
await entityService.reject(entityId);
return {'rejectedAt': DateTime.now().toIso8601String()};
})
.end('approved', name: 'Approved')
.end('rejected', name: 'Rejected')
// Flow edges
.connect('begin', 'buildChain')
.connect('buildChain', 'checkHasMoreLevels')
.connect('startLevel', 'approvalDecision')
.connect('approvalDecision', 'routeDecision')
.connect('incrementLevel', 'checkHasMoreLevels') // Loop back
.connect('makeEffective', 'approved')
.connect('handleRejection', 'rejected')
.build();Loop with Early Exit
Exit loop based on condition:
dart
builder
.oneOf('checkContinue', [
Branch.whenFn((o) => o['shouldStop'] == true, then: 'exitEarly'),
Branch.whenFn((o) => o['hasMore'] == true, then: 'loopBody'),
Branch.otherwise(then: 'normalExit'),
])
.task('loopBody', execute: (ctx) async {
// Check if we should stop
final shouldStop = someCondition();
return {
'shouldStop': shouldStop,
'hasMore': !shouldStop && moreItems(),
};
})
.end('exitEarly')
.end('normalExit');Loop with Counter
Limit iterations:
dart
builder
.task('initCounter', execute: (ctx) async {
return {
'iteration': 0,
'maxIterations': 10,
'continueLoop': true,
};
})
.oneOf('checkCounter', [
Branch.whenFn(
(o) => o['continueLoop'] == true && o['iteration'] < o['maxIterations'],
then: 'loopBody',
),
Branch.otherwise(then: 'exitLoop'),
])
.task('loopBody', execute: (ctx) async {
// Use getAny<T> for accumulated output
final iteration = ctx.getAny<int>('iteration')!;
// Do work...
return {
'iteration': iteration + 1,
'continueLoop': shouldContinue(),
};
});Best Practices
- Gateway controls entry - Don't check loop condition in task
- Upstream task sets variables - Task computes, gateway reads
- Clear exit conditions - Always have a way out
- Limit iterations - Prevent infinite loops
- Track progress - Log iteration counts
Anti-Patterns
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Loop check in task | Mixed concerns | Gateway controls loop |
| No max iterations | Infinite loop risk | Add counter limit |
| Complex gateway condition | Hard to debug | Task computes, gateway reads |
| No early exit | Stuck workflows | Add exit conditions |
When to Use Which Loop
| Approach | Use When |
|---|---|
| ForEach Loop Node | Iterating a known collection with per-item processing |
| While Loop Node | Repeating until a dynamic condition changes |
| Gateway Loop | Simple counted loops or when you need full control over flow edges |
See Loop Nodes for the native loop node types.
Next Steps
- Loop Nodes - Native forEach and while loop nodes
- Approval Workflows - Approval chains
- Error Handling - Error patterns
- Best Practices - Design guidance