Control flow
Conditions, loops, grouping, caching, and retries. How a workflow decides which steps run, how often, and how it collects results across a loop.
By default a workflow runs its actions top to bottom, once each. Control flow changes that. A step can be skipped, run once per item in a list, cached, or retried, and a group of steps can be treated as one.
The looping and grouping components (each, map, group) are where sub-actions come in. Each holds a block of child actions under its own actions key and decides how often that block runs. That is the same nesting from workflow anatomy, now doing real work, so a reusable block of steps stays one named unit you can loop, guard, or cache as a whole.
Conditions
Any action can carry an if. When the expression is falsy the step is skipped. An optional else action runs in its place.
- name: translated
component: awee.ai/translations/generate-rows
if: "{{ missing_rows.rows | default:[] | length | gt:0 }}"
vars:
rows: "{{ missing_rows.rows }}"
else:
name: translated
component: var
vars:
var: rows
value: []Note that else is a single action, not a list. The convention is to give the if branch and the else branch the same name, so downstream steps read one value ({{ translated.rows }}) regardless of which branch ran. When you catch yourself writing {{ a.x | default:b.x }} to paper over two branch names, give both branches the same name instead.
Looping with each
each runs its child actions once for every item in a list. The current item and its position are exposed under the loop step's name.
- name: insert_each
component: each
vars:
items: "{{ translated.rows | default:[] }}"
actions:
- name: row_insert
component: app:record:insert
vars:
schema: content
table: translations
record:
key: "{{ insert_each.output | key:'key' }}"
value: "{{ insert_each.output | key:'value' }}"Inside the loop, {{ insert_each.output }} is the current item and {{ insert_each.index }} is its position (an integer for a list, the key for a map). each also takes a limit to process items in chunks rather than one at a time, and a skip list of indexes to pass over.
The accumulator pattern
Each iteration overwrites the last, so to keep every result you append to a variable as the loop runs. A var step with append: true and an append_source grows a list across iterations.
- name: insert_each
component: each
vars:
items: "{{ translated.rows | default:[] }}"
actions:
- name: row_insert
component: app:record:insert
vars:
record:
key: "{{ insert_each.output | key:'key' }}"
value: "{{ insert_each.output | key:'value' }}"
- name: inserted_acc
component: var
vars:
var: rows
value: "{{ row_insert.record }}"
append: true
append_source: "{{ inserted_acc.rows | default:[] }}"Each pass appends the freshly inserted record onto inserted_acc.rows. After the loop, that accumulated list is available to later steps.
Transforming with map
Where each runs steps for their effects, map reshapes an object. It projects fields from a source into a new structure using a mapping of output keys to dotted paths.
- name: flat
component: map
vars:
value: "{{ strapi.response }}"
mapping:
model_id: "data.0.documentId"
cost_id: "data.0.ai_model_costs.0.documentId"Paths index into nested maps and arrays, and a * wildcard fans out across a list. An optional per-key template applies an expression after the mapping.
Grouping
group wraps a set of actions under one named step. It runs no logic of its own. Use it to organize a workflow, or to hang one if, retry, or cache on a whole block.
- name: enrich
component: group
actions:
- name: profile
component: http
vars: { url: "{{ env.API }}/users/{{ user_id }}" }
- name: history
component: http
vars: { url: "{{ env.API }}/users/{{ user_id }}/history" }Caching a step
A cache block stores a step's result and reuses it while the entry is fresh, keyed by the step and a hash of its resolved inputs. A cache hit skips the step entirely.
- name: summary
component: inference
cache:
for: 24h
vars:
provider: openai
model: gpt-5
prompt: "{{ var.prompt }}"Warning
The for duration uses its own units. min is minutes, h hours, d days, w weeks, and m is months. Tokens combine with spaces, so 1h 30min and 3w 2d are valid. Write minutes as min, since a bare m means a month.
Retries, delays, and explicit errors
retry re-runs a failing step. count is the number of attempts and delay is the pause between them in seconds.
- name: translated
component: inference
retry:
count: 3
delay: 10
vars:
provider: openai
model: gpt-5
prompt: "{{ var.prompt }}"Two small control components round this out. sleep pauses for a for duration given in milliseconds, useful for pacing an API. error fails the step on purpose with a message, which pairs with an if to enforce a precondition or a budget guard.
- name: budget_guard
component: error
if: "{{ estimate.cost_input_converted | gt:5 }}"
vars:
message: "estimated cost exceeds the 5.00 budget"With the flow settled, inference covers the component most workflows are built around.