Workflows

How-to recipes

Task-oriented answers. Each recipe is a self-contained pattern you can paste and adapt. For the why behind them, see the Language Guide; for exact fields, the and .

Recipes include a flow diagram whenever the pattern changes the workflow graph. Options such as retry and timeout stay code-focused because they change how one node runs, not the shape of the graph.


Get structured output from an agent

Add schema and read the typed fields back via .structured.*. Define the schema by name at file scope (before the workflow block), then reference it from the node:

schema Triage """
{ "type": "object", "properties": { "category": { "type": "string" } }, "required": ["category"] }
"""

classify = agent triager:
  "Classify: {{ input.message }}"
  schema Triage
classify -> return {
  category: classify.structured.category
}

Triage is a file-level schema definition in the same .axg file - not an external registry. For a one-off, inline the schema on the node instead:

classify = agent triager {
  prompt "Classify: {{ input.message }}"
  schema """{ "type": "object", "properties": { "category": { "type": "string" } }, "required": ["category"] }"""
}
A schema turns the agent response into fields that downstream nodes can reference.Drag to pan · scroll to zoom

Branch on an agent's decision

Use a when … / else edge; route to exactly one arm. Each arm must reach its own return.

classify = agent triager:
  "Classify: {{ input.message }}"
  schema Triage
classify -> billing when classify.structured.category == "billing"
classify -> general else
billing = agent billing_agent:
  "Handle billing: {{ classify.response }}"
  context handoff
general = agent general_agent:
  "Handle general: {{ classify.response }}"
  context handoff
billing -> return {
  via: "billing",
  reply: billing.response
}
general -> return {
  via: "general",
  reply: general.response
}

For more than two paths, chain another when/else off the else arm.

Conditional AXG edges compile into an explicit routing node.Drag to pan · scroll to zoom

Retry a flaky step with backoff

retry <n> retries on failure; after <dur> waits between attempts.

fetch = agent scraper:
  "Fetch and extract {{ input.url }}"
  retry 3 after 30s
  timeout 1m

Inside a loop, put retry on the body node, not the loop.


Keep going after a step fails

retry covers a flaky step. When one fails for good, on error routes somewhere else instead of ending the run.

  check = tool sanctions_api {
    name: input.name
  }
    retry 3 after 5s
    timeout 30s

  check -> score  when truthy(check.result)
  check -> reject else
  check -> manual on error

  manual = agent reviewer:
    "The sanctions check could not complete: {{ check.error }}. Decide manually."

  manual -> return { status: "manual_review", note: manual.response }

The route is taken only after retries are exhausted, so retry stays your first line of defence. check.error carries the reason, and a timeout routes the same way as any other failure.

One route per node. The handler is a diversion. It cannot restart the failed step, so anything that depended on check stays unrun.


Put a deadline on a step

timeout <dur> fails the node if it runs too long (then retries if configured).

slow = agent researcher:
  "Deep-research {{ input.topic }}"
  timeout 10m
  retry 1

Run steps in parallel and merge

Leave out the edge between independent nodes; raise parallel max; fan in with join all.

parallel max 2
a = agent researcher:
  "Pros of {{ input.topic }}."
b = agent researcher:
  "Cons of {{ input.topic }}."
merge = join all [a, b]
merge -> return {
  pros: a.response,
  cons: b.response
}
Independent nodes fan out, run concurrently, and synchronize at join all.Drag to pan · scroll to zoom

Take the first of several results

join any completes on the first dep to finish and cancels the rest.

fast = join any [provider_a, provider_b, provider_c]
fast -> return {
  answer: fast.output
}

Need "at least N of M"? Use join quorum 2 [a, b, c].

join any takes the first completed dependency and cancels the rest.Drag to pan · scroll to zoom

Process every item in a list

map over runs the body once per element, concurrently, and collects ordered results.

summaries = map over input.docs {
  agent summarizer:
    "Summarize: {{ $.item }}"
}
summaries -> return {
  all: "{{ $.nodes.summaries.output }}"
}

Concurrency obeys parallel max; bounded to 256 items; any item failing fails the map.

The map node owns bounded parallel fan-out and ordered collection.Drag to pan · scroll to zoom

Loop until "good enough"

loop max <n> until <expr> from <seed> repeats a step, seeing the prior result as prev.

seed = agent writer:
  "Draft about {{ input.topic }}."
draft = loop max 5 until prev.score >= 8 from seed {
  agent critic:
    "Improve and rate (return {text, score}): {{ prev.text }}"
}
seed -> draft
draft -> return {
  final: draft.text
}

Drop until to always run the full max.

The loop is one durable control node that owns its repeated critic child.Drag to pan · scroll to zoom

Pause for human approval

suspend parks the run; resume it over HTTP with a payload that becomes the node's output.

draft = agent drafter:
  "Draft a reply to {{ input.request }}"
gate = suspend: "Approve before sending?"
draft -> gate -> return {
  approved: gate.ok,
  text: draft.response
}

Resume:

curl -sX POST "$AXL_HOST/api/v1/workflow/flow/$RUN_ID/resume" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{ "node": "gate", "data": { "ok": true } }'
The suspend node parks durably until a person resumes the run.Drag to pan · scroll to zoom

Wait a fixed amount of time

delay parks the run for a wall-clock duration (durable - survives restarts).

a -> cooldown
cooldown = delay 15m
cooldown -> b
A delay is a durable control node, so the wait survives process restarts.Drag to pan · scroll to zoom

Continue an agent's conversation in the next step

context handoff makes a node continue the conversation of the node it depends on, instead of starting fresh.

research -> draft
draft = agent writer:
  "Now write the article."
  context handoff

Use context workflow to share one conversation across the whole run.

Each edge creates a durable dependency on the previous agent.Drag to pan · scroll to zoom

Call a tool and use its result

A tool node's object is the tool's arguments; its result is the node's output.

fetch = tool web_fetch {
  url: input.url,
  depth: "basic"
}
summarize = agent writer:
  "Summarize this page: {{ fetch.result }}"
fetch -> summarize
summarize -> return {
  summary: summarize.response
}

Read-only tools run inline; write tools run durably and won't double-fire on retry.

A registered tool becomes a first-class node in the workflow DAG.Drag to pan · scroll to zoom

Compose smaller workflows

A workflow node runs another registered workflow as a child. Register the child first.

call = workflow summarize_doc {
  doc: input.doc
}
call -> return {
  result: "{{ $.nodes.call.output }}"
}

The child is pinned by content hash, so re-registering it mid-run can't swap it under a live parent.

A workflow node runs a content-pinned child definition as one durable step.Drag to pan · scroll to zoom

Coordinate two runs with an event

One run emits; another waits. Events are scoped to the run's owner.

// in run A
notify = event emit "payment.confirmed" {
  order: input.order_id
}

// in run B
gate = event wait "payment.confirmed"
gate -> return {
  paid: "{{ $.nodes.gate.output }}"
}

A wait only matches events fired after it arms; bound it with timeout if it might never come.

Event nodes coordinate durable runs through owner-scoped signals.Drag to pan · scroll to zoom

Reshape data without an LLM call

transform assembles/renames data into its own output - free, no side effects.

shape = transform {
  title: research.structured.title,
  body: writer.response
}
shape -> return {
  doc: shape.output
}
A transform is a deterministic data-shaping node with no LLM cost or side effects.Drag to pan · scroll to zoom

Next

On this page