Workflows

Tutorials

Each tutorial adds one new node, building toward a full pipeline. Each tutorial is a complete, runnable workflow (the files live in the source examples directory and are compile-tested). Read them in order - each introduces one new idea.

Assumed setup: an $AXL_HOST and a $TOKEN, and agent keys/tools that exist in your deployment (swap the example keys like greeter, web_fetch for yours). Submit any of these with:

curl -sX POST "$AXL_HOST/api/v1/workflow/flow/source?name=NAME" \
  -H "Authorization: Bearer $TOKEN" --data-binary @FILE.axg

Hello, workflow (hello.axg)

The smallest useful workflow: take an input, call an agent, return its answer.

axg 1
workflow hello {
  input name
  greet = agent greeter:
    "Write a one-line friendly greeting for {{ input.name }}."
  greet -> return {
    greeting: greet.response
  }
}

New ideas: input, an agent node, {{ … }} templating, node.response, return. Try: add a second input and reference it in the prompt.

The AXG source compiles to a three-node DAG.Drag to pan · scroll to zoom

Two steps in sequence (a chain)

Feed one agent's output into the next with an edge.

axg 1
workflow writeup {
  input topic
  research = agent researcher:
    "Research {{ input.topic }} and list findings."
  draft = agent writer:
    "Write a short article from these findings: {{ research.response }}"
  research -> draft
  draft -> return {
    article: draft.response
  }
}

New ideas: an edge (research -> draft) creates a dependency; draft waits for research and reads its output. Chain as long as you like: a -> b -> c -> …. Try: add context handoff to draft so it continues research's conversation instead of starting fresh (see ).

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

Branching (triage_branch.axg)

Route to different handling based on a condition.

axg 1

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

workflow triage_branch {
  input message
  classify = agent triager:
    "Classify this support message: {{ input.message }}"
    schema Triage

  classify -> billing when classify.structured.category == "billing"
  classify -> general else

  billing = agent billing_agent:
    "Resolve the billing issue: {{ classify.response }}"
    context handoff
  general = agent general_agent:
    "Answer this general question: {{ classify.response }}"
    context handoff

  billing -> return {
    handled: "billing",
    reply: billing.response
  }
  general -> return {
    handled: "general",
    reply: general.response
  }
}

New ideas: schema Triage asks the agent for structured output, read as classify.structured.category. Triage is the file-level schema Triage """…""" definition above the workflow (in the same .axg file), referenced by name - inline schema """{…}""" on the node works too for one-offs. The when … / else edges route to exactly one arm. Each arm must reach its own return - the engine rejects a branch where one path can't complete. Try: add a third category by chaining another when/else edge off the general arm.

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

Draft-and-refine with a loop (refine_loop.axg)

Iterate a step until it's good enough (or hit a cap).

axg 1
workflow refine_loop {
  input topic
  seed = agent writer:
    "Write a first draft about {{ input.topic }}."
  draft = loop max 5 until prev.score >= 8 from seed {
    agent critic:
      "Improve the draft and rate it 0-10 (return {text, score}): {{ prev.text }}"
  }
  seed -> draft
  draft -> return {
    final: draft.text
  }
}

New ideas: a loop runs its body repeatedly. from seed seeds the first iteration's prev from the seed node; each iteration sees the previous one as prev. It stops at max 5 iterations or when until prev.score >= 8 holds. Put retry/timeout on the body, not the loop. Try: drop until so it always runs the full max iterations.

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

Fan out over a collection (map_node.axg)

Run the same step once per item, concurrently, and collect the results.

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

New ideas: map over <collection> fans the body out - one run per element, with {{ $.item }} (or item) as the current element. The node's output is the ordered array of per-item results. Bounded to 256 items. Try: pass input.docs as ["a", "b", "c"] and inspect the aggregated output.

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

Run things in parallel and merge (parallel_join.axg)

Run two independent agents at once, then combine the results.

axg 1
workflow parallel_join {
  parallel max 2
  input topic
  a = agent researcher:
    "Find pros of {{ input.topic }}."
  b = agent researcher:
    "Find cons of {{ input.topic }}."
  merge = join all [a, b]
  merge -> done
  done = return {
    pros: a.response,
    cons: b.response
  }
}

New ideas: parallel max 2 lets two durable nodes run at once. a and b have no edge between them, so they run concurrently; join all [a, b] waits for both before merge. Use join any […] to take the first to finish, or join quorum 2 […] for "at least 2 of N." Try: change to join any [a, b] and notice the other branch is cancelled once one completes.

Independent nodes fan out, run concurrently, and synchronize at join all.Drag to pan · scroll to zoom

Call a tool (tool_call.axg)

Invoke a registered tool with rendered arguments.

axg 1
workflow tool_call {
  input url
  fetch = tool web_fetch {
    url: input.url,
    depth: "basic"
  }
  fetch -> return {
    page: fetch.result
  }
}

New ideas: a tool node calls a registered tool; the object is its arguments, and the tool's result is the node's output (fetch.result). Read-only tools run inline; write tools run durably and are guarded against double-execution on retry. Try: chain fetch -> summarize where summarize is an agent reading {{ fetch.result }}.

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

Pause for human approval (human_approval.axg)

Park the run until a person resumes it.

axg 1
workflow human_approval {
  input request
  draft = agent drafter:
    "Draft a response to: {{ input.request }}"
  gate = suspend: "Approve this draft before it is sent?"
  draft -> gate -> done
  done = return {
    approved: gate.ok,
    draft: draft.response
  }
}

New ideas: suspend parks the run indefinitely. A human (or another system) resumes it with POST /api/v1/workflow/flow/{run_id}/resume and a JSON payload; that payload becomes the node's output (gate.ok here). The run survives restarts while parked. Try: resume with {"ok": true} and watch the run complete.

The suspend node parks durably until a person resumes the run.Drag to pan · scroll to zoom

Compose with a sub-workflow (sub_workflow.axg)

Call another registered workflow as a step.

axg 1
workflow parent_flow {
  input doc
  prep = tool clean {
    text: input.doc
  }
  call = workflow summarize_doc {
    doc: input.doc
  }
  prep -> call -> done
  done = return {
    result: "{{ $.nodes.call.output }}"
  }
}

New ideas: a workflow node runs another registered workflow (summarize_doc) as a child run; the object is the child's input. The child must be registered before you submit the parent - it is pinned by content hash, so a child re-registered mid-run can't silently swap under you. Try: register a tiny summarize_doc definition first, then run this parent.

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

Coordinate with events (event_driven.axg)

Emit a signal, or wait for one, across runs.

axg 1
workflow event_driven {
  input order_id
  notify = event emit "order.placed" {
    id: input.order_id
  }
  gate = event wait "payment.confirmed"
  notify -> gate -> done
  done = return {
    paid: "{{ $.nodes.gate.output }}"
  }
}

New ideas: event emit "<name>" { payload } publishes an owner-scoped event; event wait "<name>" parks until that event fires (its payload becomes the node's output). Events let separate runs coordinate - one run's emit can wake another run's wait. Try: run a second workflow that event emit "payment.confirmed" and watch this one unblock.

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

Where to go next

  • How-To Recipes - task-oriented answers (structured output, retries, error handling, parallelism, human-in-the-loop, durable timers).
  • - every node's full field set.
  • Operations Guide - submitting, polling, streaming, config, and failure modes.

Next

On this page