DocsWorkflowsComposition

Composition

Building large automations out of small ones. Calling a workflow as a step, convergent naming across branches, and the reuse patterns that keep a workflow tree readable.

A workflow is itself a component. Any workflow can call another by its id, passing variables in and reading its declared outputs back. This is what keeps a large automation manageable. You build small, single-purpose workflows and compose them, rather than growing one enormous file.

Calling a workflow as a step

Set an action's component to the id of another workflow. The caller's vars become the child's inputs, the child runs as a nested plan, and its outputs become this step's output, addressed by the step's name.

- name: translated
  component: awee.ai/translations/generate-rows
  vars:
    rows: "{{ missing_rows.rows | default:[] }}"
    language_from: "{{ var.language_from }}"
    language_to: "{{ var.language_to }}"

Here generate-rows is a whole workflow, reused as one step. Its outputs are read as {{ translated.rows }}, exactly as if it were a built-in component. A child sees only the variables the parent passes it, so a composed workflow stays isolated and testable on its own.

Because a called workflow is resolved by id, the same building block runs unchanged whether the top-level automation is launched from the command line, over the REST API, or from the web app. The composition is part of the definition, not the runner.

Convergent naming

The single habit that keeps a branching workflow readable is naming the value, not the operation, and reusing that name across branches. When an if and its else produce the same logical result, give both the same name. Downstream reads one expression.

- name: strapi
  component: http
  if: "{{ existing.rows | default:[] | length | gt:0 }}"
  vars:
    method: PUT
    url: "{{ env.API }}/models/{{ existing.rows | first | key:'id' }}"
    body: "{{ body_map | wrap:'data' | json }}"
  else:
    name: strapi
    component: http
    vars:
      method: POST
      url: "{{ env.API }}/models"
      body: "{{ body_map | wrap:'data' | json }}"

Whether the record already existed or was just created, later steps read {{ strapi.model }}. If you find yourself writing {{ existing.x | default:created.x }}, that is the signal you skipped a convergent name.

Reuse patterns

A handful of shapes recur across real automations.

  • Pipeline. A chain of steps where each names the value it produces and the next reads it. An intermediate var step gives a reshaped value a readable name between two real steps.
  • Idempotent upsert. A lookup step is the parent. Its if updates an existing record and its else creates one, both converging on the same name, so the workflow is safe to run twice.
  • Fan-out. An each loop that calls a sub-workflow once per item, accumulating the results. This is how a batch job reuses a single-item workflow.
  • Orchestrator. A top-level workflow whose steps are almost all calls to other workflows, wiring them together and shaping the final output.
  • Chunk, process, unchunk. Split a large input into chunks, run a workflow over each, then reassemble. This keeps a model call or an API request within its size limits.

Events for milestones

Long automations emit their own milestones with event:emit, publishing a named event other systems or steps can react to. A correlation id threaded through the run's variables ties every emission back to one execution, so a workflow driving real work stays observable from the outside.

A composed automation can span many workflows. Lifecycle shows how the engine folds that whole tree into one static plan, runs it, and accounts for every step it touches.