Language guide
AXG (AXL Graph) is a small declarative language for describing a workflow as a graph. This guide explains how it works conceptually. For exact syntax, see the and .
A workflow is a graph
You declare nodes (the steps) and edges (the dependencies between them). The engine computes the order: a node runs once all the nodes it depends on have finished, and independent nodes run in parallel (up to a concurrency cap).
axg 1
workflow example {
input topic
a = agent researcher:
"Research {{ input.topic }}." // a node
b = agent writer:
"Write it up: {{ a.response }}" // another node
a -> b // an edge: b depends on a
b -> return {
article: b.response
} // finish
}The graph must be a DAG - no cycles - and at least one path must reach a return. Both are
checked when you submit; you can't run a graph that could deadlock.
File structure
axg 1 // version header — required, must be `axg 1`
schema <Name> """{ … }""" // optional: named JSON Schema(s), at file scope before the workflow block
workflow <name> {
input <name> // zero or more inputs (read as input.<name>)
parallel max <n> // optional: concurrency cap for this workflow
<id> = <node> // node declarations (any order)
<id> -> <id> // edges (any order)
}- Node ids are
[A-Za-z0-9_-]; they can't start with_axg_(reserved for compiler-generated nodes). - A
schema <Name> """{ … }"""definition may appear at file scope, before theworkflowblock;agent/modelnodes then reference it by name (schema <Name>). These are definitions in the same.axgfile, not an external registry. //and/* … */are comments.- Declaration order doesn't matter - the graph is defined by the edges, not the text order.
Nodes
Every node has a kind (what it does) and a unique id (how other nodes refer to it). The kinds:
| Kind | One-liner |
|---|---|
agent | run a registered agent with a prompt |
model | one agentless LLM call |
tool | invoke a registered tool |
branch | route to one of two paths based on a condition |
transform | reshape data inline (no side effects) |
return | finish the run with an output |
delay | pause for a wall-clock duration |
suspend | pause until a human resumes it |
loop | repeat a step to a cap or until a condition |
map | run a step once per item in a collection |
join | wait for several upstream nodes to finish |
event | emit or wait on a named event |
workflow | run another registered workflow as a child |
Most nodes have a short form (id = agent key: "prompt" attr …) and some have a block form
(id = agent key { prompt "…" attr … }). Full details in the .
Passing data: references
Nodes don't share variables - they read each other's outputs by reference. A reference is a dotted path:
| Reference | Reads |
|---|---|
input.topic | the run input field topic |
input.config.region | a nested input field |
<node>.response | an agent/model node's text answer |
<node>.structured.field | a node's structured (schema'd) output field |
<node>.<field> | any field of a node's output (e.g. a tool's <node>.result) |
prev.x | (inside a loop body) the previous iteration's output |
item | (inside a map body) the current collection element |
Two rules apply, and only one of them is enforced by the compiler:
- The node must exist. A typo'd node id is a compile error, not a silent empty value. This is enforced at submit.
- You read a node's output -
node.response,node.structured.*, ornode.<field>- or itserroron anon errorroute. Nothing else about a node is readable from the graph.
The field is not checked, and cannot be: a node's output shape is decided at runtime by the
agent's schema or the tool's result. So a.status compiles - the bare-field sugar reads it as an
output field named status - and then resolves to nothing when the node runs. Run-state lives in
the run document, not in the graph.
Under the hood these compile to canonical $. JSON paths (input.topic → $.input.topic,
a.response → $.nodes.a.output.response). You can write the $.-form directly too; both are valid.
Templates
Inside any string, {{ <reference> }} is a template placeholder spliced at runtime:
summary = agent summarizer:
"Summarize {{ input.topic }} using {{ research.response }}."If a placeholder can't resolve when the node runs (e.g. it points at data that isn't there), the node
fails loudly rather than sending a literal {{ … }} to the agent. Template references are
validated at submit just like everything else.
Conditions: the branch expression language
branch (with when …) and loop (with until …) take a boolean expression:
route -> approve when truthy(review.structured.passed) && score >= 8
route -> reject else| Form | True when |
|---|---|
truthy(ref) | present and non-empty / non-zero / true |
exists(ref) | present at all (including an explicit null) |
ref == value | strictly equal (1 ≠ 1.0 ≠ "1") |
ref >= n (>, <, <=) | numeric comparison |
a && b, a || b, !a / not a, ( … ) | boolean composition |
When something fails
A node that fails takes down the run, which is usually what you want: you find out loudly. Three things change that, in order of how much you should reach for them.
Retry handles a flaky step. The node runs up to n times in total.
check = tool sanctions_api {
name: input.name
}
retry 3 after 5s
timeout 30sretry 3 is three attempts in total - the first try and two more, five seconds apart -
not three retries on top of the first. retry 1 therefore means no retry at all.
on error handles a step that failed for good. Once retries are exhausted, the
run continues down a route you choose instead of ending:
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."check.error is the failure reason. A timeout is a failure, so it routes the same
way. Each node takes at most one on error route, and the handler is a diversion: it
cannot restart the failed node or stand in for its output, so anything that depended
on check stays unrun.
A handled failure is not a failed run. A run that took its on error route and reached
a return finishes completed, because it completed the path you authored - and in
axl workflow list it looks exactly like a run that never went wrong. Alerting on
status != completed will therefore never tell you the vendor was down.
What records it is the run document: the failed node keeps its own failed status and
its error, so axl workflow status <run_id> shows the diversion even though the run
is green. If a diversion is something you want to hear about, say so in the handler -
have it emit, or return an output field your caller checks:
manual -> return { status: "manual_review", reason: check.error }join any handles the case where you have several ways to get a result and only
need one. A failed branch does not sink a join whose threshold is still reachable.
Without any of these, a failed node fails the run and its error lands on the run's output. That is the default and it is a reasonable one - route only where you have something better to do than stop.
The lifecycle: compile → submit → run
- Compile. Your
.axgis parsed and lowered to a canonical JSONWorkflowDefinition, then validated (references resolve, the graph is acyclic, every branch reaches a return, sizes and durations are within bounds).axl workflow checkruns exactly this, locally. - Submit. You POST the source (or the JSON). The engine creates a durable run, returns a
run_idimmediately, and queues it. - Run. A background consumer advances the run in small steps ("pulses"), launching nodes, collecting their outputs, and persisting state after every step. The run survives restarts, and multiple server instances can share the work.
- Finish. When a
returnnode runs, the run iscompletedand itsoutputis the return body. A node that exhausts its retries (or whose run is cancelled) ends the runfailed/cancelled.
You never manage any of step 3 - you submit, then poll or stream.
What you get for free
- Durability. Run state lives in Redis; a crash mid-run resumes from the last committed step.
- Exactly-once side effects (common case). Write tools and child launches are guarded so a retry/restart doesn't double-fire them.
- Retries & timeouts. Per-node
retry/timeout; a node past its deadline fails (and retries if configured). - Fan-out & fan-in.
map,parallel, andjoinexpress concurrency without you wiring threads. - Fail-fast authoring. Bad references, cycles, unreachable returns, oversized values, and impossible joins are all rejected at submit.
Next
- Tutorials - apply all of this to build a pipeline.
- / - the exact syntax.