Skip to content

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

  1. Gateway controls entry - Don't check loop condition in task
  2. Upstream task sets variables - Task computes, gateway reads
  3. Clear exit conditions - Always have a way out
  4. Limit iterations - Prevent infinite loops
  5. Track progress - Log iteration counts

Anti-Patterns

Anti-PatternProblemSolution
Loop check in taskMixed concernsGateway controls loop
No max iterationsInfinite loop riskAdd counter limit
Complex gateway conditionHard to debugTask computes, gateway reads
No early exitStuck workflowsAdd exit conditions

Next Steps