DocsWorkflowsCookbook

Cookbook

Three real workflows dissected end to end. A structured-output translation, a diff-and-fill batch job that composes it, and a multi-stage research agent.

Every idea in these guides comes together in a complete workflow. Here are three real ones, walked through from the first step to the final output.

1. Translate a batch of rows

This workflow takes a list of { key, value } rows and translates every value with a single forced tool call. The tool's JSON schema is the output contract, so the model returns structured data, not prose.

id: monimoto/translations/generate-rows
name: Translate a batch of translation rows

vars:
  ai_model: gpt-5
  ai_provider: openai
  language_from: en
  language_to: lt
  context: ""
  prompt: file:./generate_rows.md
  rows: []

actions:
  - name: translated
    component: inference
    cache:
      for: 24h
    retry:
      count: 3
      delay: 10
    vars:
      provider: "{{ var.ai_provider }}"
      model: "{{ var.ai_model }}"
      prompt: "{{ var.prompt }}"
      prompt_vars:
        language_from: "{{ var.language_from }}"
        language_to: "{{ var.language_to }}"
        context: "{{ var.context }}"
        rows: "{{ var.rows | default:[] }}"
      tools:
        - translate_rows
      tool_choice: specific

  - name: output
    component: var
    vars:
      var: rows
      value: "{{ translated.tools | key:'translate_rows' | from:'json' | key:'rows' | default:[] }}"

outputs:
  rows: "{{ output.rows }}"

Reading it top to bottom:

  • Variables are the inputs. vars declares defaults for the model, the languages, and the rows. A caller overrides them. The prompt points at a file, so the tool definition and the system and user prompts live in their own template.
  • One inference does the work. tool_choice: specific forces the translate_rows tool, so the model must return data matching that tool's schema. cache: { for: 24h } means an identical batch is free to re-run within a day, and retry: { count: 3, delay: 10 } rides out a transient provider error.
  • The result is read out of tools. The output step pulls the translate_rows entry, parses it from JSON, and takes its rows field. That parsed list is the workflow's declared output.

The whole automation is two steps. All of the structure is in the prompt file's schema and the single modifier chain that unpacks the answer.

2. Translate only what is missing

The batch translator above is a building block. This second workflow finds which rows are missing in a target language, translates just those by calling the first workflow, and inserts each result. It shows chaining, a guard, composition, and a loop accumulator together.

id: monimoto/translations/generate-missing
name: Fill in missing translations

actions:
  - name: base_rows
    component: app:records:query
    vars:
      schema: content
      table: translations
      filter: { language: "{{ var.language_from }}" }

  - name: base_keys
    component: var
    vars:
      var: keys
      value: "{{ base_rows.rows | default:[] | pluck:'key' }}"

  - name: missing_rows
    component: app:records:query
    vars:
      schema: content
      table: translations
      filter:
        language: "{{ var.language_to }}"
        key: { op: not_in, value: "{{ base_keys.keys }}" }

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

  - 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' }}"
            language: "{{ var.language_to }}"
      - name: inserted_acc
        component: var
        vars:
          var: rows
          value: "{{ row_insert.record }}"
          append: true
          append_source: "{{ inserted_acc.rows | default:[] }}"

outputs:
  inserted: "{{ insert_each.inserted_acc.rows | default:[] }}"
  • A four-step pipeline computes the difference. Query the base language, pluck its keys, then query the target language for rows whose key is not_in that set. Each step names the value it produces, so the next reads it plainly.
  • A guard protects the expensive step. if: "{{ missing_rows.rows | default:[] | length | gt:0 }}" means the model is only called when something is actually missing.
  • It composes the first workflow. component: awee.ai/translations/generate-rows reuses workflow #1 as a single step. No translation logic is duplicated.
  • The loop accumulates. insert_each inserts each translated row and appends it onto inserted_acc.rows. The final output reaches into that in-loop accumulator by short name.

3. A research agent

The same primitives scale up to a multi-stage agent. A web-search workflow chains three inferences with the fan-out and accumulator patterns between them.

  1. Extract intent. A forced tool call turns the user's question into a list of search queries, read back as {{ keywords.tools | key:'web_search_keywords' | from:'json' | key:'queries' }}.
  2. Gather sources. An each over those queries runs search:web and crawler, appending every result into a single accumulated list. Each network step is cached, so re-running the agent does not re-fetch.
  3. Select and read. A second inference picks the most relevant URLs from the gathered set, and a second crawl fetches their full content.
  4. Compose the answer. A final inference writes the response from the collected material, and the workflow surfaces both the answer and its references as outputs.

Around those steps, event:emit publishes a milestone as each stage completes, and a correlation id threaded through the variables ties every event and every priced inference back to the one run. The result is an agent that is still a static plan. You can see every stage it will run, and account for every token it spends, before it starts.

That is the shape of the engine. Small workflows, wired by their outputs, composed into larger ones, each one inspectable and priced. Browse the component reference for the full menu of steps, or start back at workflow anatomy.