Skip to content

Iteration and Parallelism

for_each iterates a list. max is required, and declares the bound.

- attachments:
for_each: "{{ message.attachments }}"
as: file
max: 25
concurrency: 4
on_error: continue # continue | fail_fast
steps:
- scan: { av.scan: { url: "{{ file.url }}" } }
- store:
when: "{{ scan.clean }}"
s3.put: { key: "{{ message.id }}/{{ file.name }}" }

The construct binds its id to a list of per-iteration scopes, indexed by position: {{ attachments[0].scan.clean }} and {{ attachments | length }}.

Each iteration creates a scope. No binding crosses from one iteration to the next. The loop binding that as names is visible inside the construct only.

Iteration order follows the source list. concurrency affects scheduling only, and MUST NOT affect results.

A source list longer than max fails the step before any iteration runs. Implementations MUST NOT truncate a source list.

max accepts a positive integer or the word unbounded. unbounded declares that the source list’s own length is the bound. An omitted max is a validation error: the author states the bound in every case. The source list is finite when the step starts, so the loop terminates under either value. unbounded-iteration is a capability, so a deployment can decline to execute an unbounded loop.

Failure within an iteration. on_error declares what a failed step inside one iteration does to the construct. It takes one of two values.

Value Behavior
fail_fast The failure fails the construct. Iterations that have not started do not start, and iterations in progress are cancelled. Default.
continue The failing iteration stops at its failed step. Other iterations run to completion, and the construct succeeds.

A step of an iteration that did not run is recorded with the outcome not_run, and a reference to it resolves to null.

Every iteration scope holds a reserved error binding.

Path Type Meaning
error object? null when the iteration completed. Otherwise an object.
error.step string The id of the step that failed.
error.code string The error code, from the manifest’s error table or from the fault vocabulary.
error.message string A human-readable description. Its text is implementation-defined.

Failed iterations are therefore selected with the ordinary filters: {{ attachments | keep(current.error != null) }}.

parallel runs named branches and joins them implicitly.

- enrich:
parallel:
crm: [{ lookup: { crm.find_contact: { email: "{{ message.from }}" } } }]
billing: [{ lookup: { billing.account: { email: "{{ message.from }}" } } }]
timeout: 30s
on_error: continue # continue | fail_fast

parallel binds its id to a scope whose members are the branch scopes, named by branch: {{ enrich.crm.lookup.deal_size }}. Branch names MUST match ^[a-z][a-z0-9_]*$ and MUST be unique. A branch MUST NOT reference another branch’s bindings.

Branch scheduling affects the order of external effects only, and MUST NOT affect results. The order in which branches contact external systems is unspecified.

on_error takes the values and the meanings that for_each defines. Each branch scope holds the same reserved error binding.