For-Each Loops
Steps can iterate over a list using for_each, running the same action for each item. This enables fan-out/fan-in patterns: expand a step into N parallel (or sequential) instances, then converge when all are done.
Overview
Section titled “Overview”- Fan-out: A
for_eachstep is expanded into N instance steps at runtime, one per item in the list - Fan-in: Downstream steps wait for all instances to complete. The placeholder step’s output is an array of all instance outputs
- Parallel by default: All instances run concurrently. Use
sequential: truefor one-at-a-time execution eachvariable: Inside loop instances,each.itemis the current array element andeach.indexis the zero-based index- Composable: Combine with
whenconditions,type: taskactions, andcontinue_on_failure
YAML Syntax
Section titled “YAML Syntax”Parallel loop (default)
Section titled “Parallel loop (default)”actions: get-items: type: script script: echo 'OUTPUT: {"items": ["a","b","c"]}' process-item: type: script script: | echo "Processing {{ input.item }}" echo 'OUTPUT: {"result": "done-{{ input.item }}"}'
tasks: main: flow: fetch: action: get-items
process: action: process-item depends_on: [fetch] for_each: "{{ fetch.output.items }}" input: item: "{{ each.item }}" index: "{{ each.index }}"
aggregate: action: combine depends_on: [process] input: results: "{{ process.output }}"When the job runs:
fetchcompletes and outputs{ "items": ["a", "b", "c"] }processexpands intoprocess[0],process[1],process[2]— all run in parallelaggregatewaits for all 3 instances, then seesprocess.outputas[{"result":"done-a"}, {"result":"done-b"}, {"result":"done-c"}]
Rendering the array
Section titled “Rendering the array”for_each must render to a JSON array. Render arrays and objects with
| json_encode() — it always produces valid JSON, whereas a bare {{ x }}
renders Tera’s display format ({"k": v} for objects, floats with .0):
for_each: "{{ fetch.output.items | json_encode() }}"Literal array
Section titled “Literal array”flow: deploy: action: deploy-to-region for_each: ["us-east-1", "eu-west-1", "ap-south-1"] input: region: "{{ each.item }}"Sequential loop
Section titled “Sequential loop”Use sequential: true to run instances one at a time, in order:
flow: migrate: action: run-migration for_each: "{{ fetch.output.schemas }}" sequential: true input: schema: "{{ each.item }}"In sequential mode:
- Instance
[0]starts immediately;[1]starts after[0]completes, etc. - If an instance fails (and
continue_on_failureis not set) every remaining instance is skipped immediately and the loop step fails. - Useful for ordered operations like database migrations
The each Variable
Section titled “The each Variable”Inside loop instances, two special variables are available:
| Variable | Type | Description |
|---|---|---|
each.item |
any | The current array element for this iteration |
each.index |
number | Zero-based index (0, 1, 2, …) |
These are available in input templates and action templates (env, script, cmd).
Fan-In and Output Aggregation
Section titled “Fan-In and Output Aggregation”When all loop instances complete, the placeholder step’s output becomes an array of all instance outputs, ordered by index:
flow: process: action: compute for_each: ["a", "b", "c"]
summary: action: aggregate depends_on: [process] input: # Array of outputs: [output_0, output_1, output_2] all_results: "{{ process.output }}"- Failed instances always contribute
nullto the output array — visible unconditionally, with or withoutcontinue_on_failureon the placeholder (a dependent’s ownacceptdecides whether it ever sees that output at all) - Skipped instances also contribute
null
Combining with when Conditions
Section titled “Combining with when Conditions”when is evaluated before for_each. If the condition is falsy, the step is skipped entirely (no expansion):
flow: deploy: action: deploy-to-region for_each: "{{ fetch.output.regions }}" when: "{{ input.should_deploy }}" input: region: "{{ each.item }}"Sub-Job Fan-Out (type: task + for_each)
Section titled “Sub-Job Fan-Out (type: task + for_each)”A for_each step can reference a type: task action — each iteration creates a separate child job:
actions: deploy-env: type: task task: deploy-pipeline
tasks: main: flow: deploy-all: action: deploy-env for_each: ["staging", "prod"] input: environment: "{{ each.item }}"
deploy-pipeline: input: environment: { type: string } flow: setup: action: init-env deploy: action: run-deploy depends_on: [setup]Each instance creates a full child job with its own steps, logs, and lifecycle. The called task can itself use for_each — enabling nested iteration through separate job scopes.
Error Handling
Section titled “Error Handling”| Scenario | Behavior |
|---|---|
| Empty array | Step is skipped (not failed) |
| Non-array result | Step fails with error: for_each must render a JSON array; the rendered text (N bytes) is not valid JSON … or … got a JSON object (the error describes the shape only, never the rendered text) |
| Instance failure (parallel) | All instances run regardless (nothing to sequence); placeholder rolls up failed once all are done — regardless of continue_on_failure |
| Instance failure (sequential) | Without continue_on_failure on the placeholder, remaining pending instances are skipped immediately. With it, the loop keeps promoting instances past the failure. Either way, the placeholder rolls up failed once done — the flag no longer changes the rollup itself |
Template error in for_each expression |
Step fails with error |
continue_on_failure on the loop’s own placeholder still decides whether a failed instance stops a sequential loop from promoting further instances (same as any continue_on_failure effect pre-0.18) — but it no longer changes the final rollup: any failed instance makes the placeholder roll up failed, always, cof or not. The flag’s remaining effect on job status is also unchanged: it decides whether that failure fails the job — self-scoped, like any other step. A dependent that must still run despite a failed loop needs its own accept: [completed, failed] (or accept: any) on the edge to the placeholder; without it, the placeholder’s failed status skips the dependent unreachable the same way any other untolerated failure would.
Limits
Section titled “Limits”- Maximum 10,000 items per
for_eachexpression - Step names must not contain
[or](reserved for instance naming) - Nested
for_eachwithin the same task is not supported — usetype: taskfor nesting