Loop Patterns
Patterns for iterative workflow execution.
Gateway-First Loop Pattern
The recommended approach: 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 |
Next Steps
- Approval Workflows - Approval chains
- Error Handling - Error patterns
- Best Practices - Design guidance