Skip to content

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

dart
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:

json
{
  "id": "processItems",
  "type": "forEachLoop",
  "name": "Process Each Item",
  "config": {
    "schemaType": "config.forEachLoop",
    "collectionPath": "items",
    "loopId": "itemLoop",
    "storeAs": "processedItems"
  },
  "nodeIds": ["validateItem", "transformItem"]
}

ForEach Properties

PropertyTypeDescription
collectionPathStringPath to the collection in accumulated output
loopIdStringUnique identifier for this loop's state
storeAsString?Key for storing loop results in output
simulationModeStrategy for simulation (optional)

Output Ports

ForEach loops have two output ports:

PortIDDescription
IterationiterationTaken for each item in the collection
CompletedcompletedTaken when all items are processed

Loop State

Inside child nodes, access the loop state via ExecutionContext:

dart
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

PropertyTypeDescription
indexint0-based iteration index
itemdynamicCurrent collection item
keydynamicKey for the current item (for maps)
totalintTotal number of items
isFirstboolWhether this is the first iteration
isLastboolWhether this is the last iteration
collectiondynamicThe full collection being iterated

While Loop

Repeats child nodes while a condition evaluates to true.

Definition

dart
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:

json
{
  "id": "retryUntilSuccess",
  "type": "whileLoop",
  "name": "Retry Until Success",
  "config": {
    "schemaType": "config.whileLoop",
    "conditionPath": "shouldRetry",
    "loopId": "retryLoop",
    "storeAs": "retryResult"
  },
  "nodeIds": ["attemptTask", "checkResult"]
}

While Properties

PropertyTypeDescription
conditionPathStringPath to a boolean in accumulated output
loopIdStringUnique identifier for this loop's state
storeAsString?Key for storing loop results in output
simulationModeStrategy for simulation (optional)

Loop State

dart
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

PropertyTypeDescription
iterationint1-based iteration count

LoopContext Extension

The LoopContextExtension on ExecutionContext provides convenient accessors:

dart
// 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:

dart
// 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:

dart
// 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:

dart
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:

dart
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

AspectForEach LoopWhile LoopGateway Loop
Use caseIterate a collectionRepeat until conditionManual loop control
State managementAutomatic (ForEachLoopState)Automatic (WhileLoopState)Manual (task output)
TerminationAll items processedCondition becomes falseGateway branch
Child nodesnodeIds in containernodeIds in containerStandard edges
Builder supportJSON / direct constructionJSON / direct constructionWorkflowBuilder
Best forKnown collectionsPolling, retrySimple counted loops

Best Practices

  1. Use forEachLoop for collections - Automatic item tracking and index management
  2. Use whileLoop for conditions - Polling, retry-until-success patterns
  3. Use gateway loops for simple cases - When you don't need structured loop state
  4. Always set loopId - Required for accessing loop state in child tasks
  5. Use storeAs - Namespace loop results in the workflow output
  6. Guard against infinite while loops - Include an iteration limit in the condition logic
  7. Access loop state via extensions - Use context.forEachLoop(id) and context.whileLoop(id)

Next Steps