DocsWorkflowsControl flow

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.