Documentation — Workflow language reference
Reference · Workflows

Workflow language reference

This page lists the rules of the workflow document beyond the node kinds: its top-level keys, durations and rates, templates and CEL, statuses, limits, load-time errors, the build features that nodes depend on, and the tools and A2A commands that manage workflows at runtime. It is for builders writing workflows and for operators reading a load error. For each node kind's fields, see the Workflow node reference.

The workflow document

A workflow is one entry of workflows: in a configuration file, or a standalone file the configuration points at. Its keys are a closed set; an unknown key is a parse error.

KeyMeaning
namerequired; [a-zA-Z_][a-zA-Z0-9_-]{0,63}; one definition per name
descriptionfree text; a workflow tool's description
stepsrequired; a map of step id to step
armeddefault true; false loads the definition without arming its start nodes
inputs{schema}: a JSON Schema each run's inputs must match; a mismatch creates no run
outputs{schema}: a completed run's output must match, or the completing finish fails
concurrency{max_runs, on_overflow, scope}: default 4 live runs (1 to 1024), queue, workflow. on_overflow is queue, drop or replace (cancel the oldest live run); scope: key counts runs per key
keya template rendered from the trigger payload naming what a run is about; required by scope: key
limits{steps, tokens, deadline, budget} for each run. steps and tokens default to limits.run.*; budget is a budget scoped to the run, as in Control spend with budgets
prioritylow, normal (default) or high: under resource pressure low admissions shed first, and ready steps of higher-priority runs are scheduled first
durablefalse keeps runs in memory only; unset follows store.durability.work (default durable)
state{<var>: {schema?, reducer?}}: a schema every write to the run variable must match, and how concurrent writes combine (overwrite, append, merge, union)
unload{policy, timeout}: what retirement does to live runs: drain (default; timeout bounds it, then the rest are cancelled), cancel or detach
tool{name, mode, grant}: register the workflow as a tool; configuration only. mode is sync (default) or async; grant keys are root, workflows, subagents, user, agent

A workflows: entry can name a document instead of holding it:

EntryLoads
{name, file: <path>}a file on disk
{name, url: <https URL>, headers?, timeout?, allow_private?}a document fetched at startup; unreachable fails the load
{name, uri: <MCP resource>}a resource a connected MCP server serves
{dir: <path>} or {dir: {path, glob?, order?}}every matching file in a folder; glob defaults to *.yaml,*.yml,*.json (** recurses), order is name (default) or date

A standalone file gets editor completion from the published schema with # yaml-language-server: $schema=https://agentd.dev/schema/workflow.json as its first line. agentd --workflow-schema prints the same schema from the binary you run.

Durations and rates

ValueAccepted forms
a durationan integer and a unit: 500ms, 30s, 10m, 2h, 1d, 1w; a bare number is seconds. In a workflow field also {ms: <n>}. No compound forms such as 1h30m
a rate"<burst>/<per>": "20/1s", "10/1m", "1/1d"; a bare or s/sec window is seconds

Templates

Every field of a step is rendered against the run's data immediately before the step runs, except the fields the step evaluates itself (below).

RuleExampleResult
A string that is exactly one placeholder yields the typed value"{{ steps.fetch.output.json }}"the object
Any other string interpolates; strings raw, other values as JSON"id={{ inputs.id }}""id=42"
`` supplies a fallback for a missing path, parsed as JSON if it parses`"{{ inputs.limit
A missing path with no fallback fails the step"{{ steps.fetch.outptu }}"step error
A leading / reads a JSON-pointer path, for ids with dots"{{ /steps/refund.when/output }}"the guard's answer
CEL: evaluates an expression over the same names"CEL: size(steps.list.output) > 0"true
{{secret:NAME}} and {{secret-file:PATH}} are left as written"Bearer {{secret:TOKEN}}"unchanged

Things to know about the rules:

  • A fallback applies only when a path is missing. A step that was pruned, skipped or has not run yet has output: null, which is present, so the fallback does not apply.
  • A render error happens after the attempt counter moved, so it uses up one retry.
  • Paths are not checked at load. A typo is a step failure when the step runs.
  • Secret references resolve only where a node resolves them: an http step's headers and sign.secret, and a webhook's auth. Anywhere else the destination receives the literal text. A credential never lands in step data or logs.
  • An output larger than limits.inline_max_bytes (default 65 536 bytes) is stored as an artifact and replaced by {"$artifact": <id>, "size": <n>} in the run record.

Namespaces

NameHolds
inputsthe run's validated inputs
runid, workflow, start, principal, task, attempt, status
steps.<id>status, output, error, attempt; inside a body, siblings by their unscoped id
varsthe run variables assign, transform and collect wrote
envinstance, run, ts (the current time, in ms), instruction, prompt; per step also step, attempt and idempotency_key
memory.<key>a durable memory key, read each time a step renders, for the keys the definition names
item, index, the as aliasthe current element in a foreach, batch, map, filter or reduce
batch, iteration, branchthe batch number, the iterate iteration, the parallel/race branch name

A start node's inputs mapping is rendered against a different pair: payload (the trigger payload) and env.

env.idempotency_key is the same on every retry and replay of the step; env.ts changes every attempt. Use the first for anything that must be stable.

Fields that are not rendered

These are evaluated by the step itself or hold nested definitions, so they are passed as written: assert.condition, validate.schema, map/filter/reduce.expr, iterate.while/until, every body and branches, the filter of subscribe, signal and event, wait.condition, wait.match, think.check and switch.cases.

CEL

CEL expressions need the cel build feature, which the release binaries include. A build without it refuses a configuration that uses CEL (exit 2) rather than skipping the expression. In a field that is always CEL, the CEL: prefix is optional.

WhereSees
a step's whenthe run's data: inputs, run, steps, vars, env, memory
assert.condition, a CEL: templatethe run's data
map/filter/reduce.exprthe run's data, item, index, the as alias; reduce adds acc and its alias
iterate.while, iterate.untilthe run's data, iteration, last, results
loop.untiloutcome ({ok, output}) and last
filter on signal and eventpayload
filter on subscribecontent
wait.matchevent, inputs, vars
wait.conditionruns, subagents, now_ms, signals only

A CEL error in a when guard fails the run. When a guard may meet missing data, put the value in a run variable first with an assign whose template has a fallback, and test the variable.

Statuses

Run statusTerminalReached by
runningnothe normal state, even when every step is suspended
pausednoan operator hold, or a breakpoint
completedyesa finish with the default status
failedyesan unhandled step failure, a run limit or deadline, or a finish with another status
refusedyesa finish with status: refused, or a restore that found neither the pinned definition nor one with its hash
cancelledyesan operator or a parent run, or a finish with status: cancelled
stalledyesnothing ready, nothing in flight, no failed step, and no finish reached
Step statusTerminalSatisfies dependentsMeaning
pendingnononot started
runningnonoin progress, checkpointed
suspendednonowaiting on a durable record: a wait, sleep, gate, timer or retry backoff
doneyesyesfinished, or failed under on_error: continue
skippedyesyesa sibling start node fired, on_replay: skip, or a false guard inside a body
prunedyesnoa guard declined, or a branch nobody chose
failedyesnoan error
timeoutyesnoits deadline passed
cancelledyesnothe run ended first, or a race was lost

When a workflow run decides how a one-shot process exits, its status maps to the exit code: completed 0, refused 5, stalled 3, cancelled 1, and failed by its cause: 7 for an exhausted budget, 124 for a passed deadline, 4 for an unreachable model, 1 otherwise. The full table is in CLI, environment and exit codes.

Limits

LimitValue
node kinds74: 11 start nodes, 63 steps
top-level steps per workflow512 (body steps not counted)
body nesting depth4
step id64 characters, [a-zA-Z_][a-zA-Z0-9_-]{0,63}
retry.maxcapped at 20; backoff doubling stops growing after 10 doublings
step timeout when unsetlimits.step_timeout, 600 s (model steps, mcp.tool)
http timeout when unset30 s
a2a.send / a2a.delegate timeout when unset30 s / 120 s
human gate timeout when unset24 h
wait with no timeoutwaits indefinitely
live runs4 per workflow (1 to 1024), 8 per instance (limits.max_runs)
foreach/batch parallel lanesdefault 4, at most limits.workflow.fan_out (8 when unset)
foreach / batch group sizedefault 1 / 10
iterate.max_iterationsdefault and cap 10 000
subscribe.window.samples1 to 256
stream.batch.size2 to 1000
correlate.max_pendingdefault 1000, at most 100 000
decide16 questions, 2 to 32 options or levels each
chained message deliverieslimits.max_message_depth, default 8
inline output sizelimits.inline_max_bytes, default 65 536 bytes

Load-time errors

agentd --validate-config reports every error at once on stderr as config.invalid lines and exits 2. The messages for workflows:

Message (abridged)Cause
unknown field "prompt" for kind "agent" (allowed: …)a field the kind does not take; the message lists the ones it does
kind "agent" requires field "instruction"a required field is missing
a \finish` step is required`no finish in the workflow
depends_on names unknown step "x"a typo in depends_on
a non-start step must depend on something (unreachable root)a step with no depends_on that is not a start node or an on_timeout target
not reachable from any start nodeno path from a start node
cycle among steps [...]the graph is not acyclic
on_timeout names unknown step, on_error goto names unknown stepa routing edge to a step that does not exist
switch case "a" must name ONE step as a string (got a list)cases values are step ids, not lists
`on` is fixed by the definition … so it cannot be templateda templated wait.on
concurrency.scope: key needs a \key:` template`scope: key without key
workflow "x" is defined twice — by … and by …two sources define one name
a schedule needs \cron`, `every` or `at``a schedule that could never fire
parallel 16 exceeds limits.workflow.fan_out (8)a fan-out over the ceiling
on_unsure: escalate, but its model (smart) names no \escalate` model`nothing to escalate to
a start node cannot carry a decision guarda when: {noul} on a trigger
human.to: \to` names role user, who could never see the task`a gate addressed to someone who cannot see it
a \webhook` node (start or wait) is used but webhooks.listen is not set`no webhook listener
subscribes to "…" on mcp server "…", which no \event_grants` entry grants`a subscribing wait outside an operator grant

Some checks need the live registry and run at startup rather than in --validate-config; each exits 2:

MessageCause
tool "x" is unknown, disabled or not granted to workflowsa tool step names a tool workflows cannot call
mcp server "x" is not connectedan mcp.tool step names a server that is not declared, or did not connect at startup
stream "x" is not declared under \streams:``a stream start on an undeclared stream

Build features

A node or field that needs a feature the binary was built without fails plainly: refused at load, or the step fails naming the feature. The release binaries and the container image include every feature below.

FeatureNeeded by
celevery CEL expression: when as a string, assert, map/filter/reduce, iterate, loop.until, start-node filter, wait.match, wait.condition, CEL: templates
cronschedule.cron
a2athe webhook start and webhooks.listen, wait {on: webhook}, a2a.send, a2a.delegate, the a2a start, and every human gate that is asked over the A2A listener
mcp-eventswait {on: event, subscribe: …}

Build features and crates lists every feature.

Managing workflows at runtime

The agent manages workflows through built-in tools. Each tool's default grant says which callers are offered it: the agent's own turns (root), workflow steps, and subagents.

ToolCallersArgumentsResult
workflow.runroot, workflowsname, inputs, start, wait, timeout{run, status, output?, task?}
workflow.statusallrun or name{runs}
workflow.listall{workflows}
workflow.waitroot, workflowsrun, timeout{run, status, output}
workflow.cancelroot, workflowsrun, reason{ok, status}
workflow.signalallname, payload, run{delivered: 0, note}
workflow.pauserootrun, name, before_step{ok, break_before?}
workflow.resumerootrun, name{ok}
workflow.createrootdefinition, arm{name, hash, armed}
workflow.updaterootname, definition{name, hash}
workflow.deleterootname{ok}
  • workflow.signal as a tool only records the signal on the durable inbox; waits and signal start nodes are woken on the next reactor pass, so delivered is always 0. Retrying because it was 0 sends the signal twice. The workflow.signal step delivers at once and counts.
  • workflow.pause with run holds one run; with name, it disarms the workflow's start nodes. before_step sets a durable breakpoint: the run continues and pauses immediately before that step starts.
  • workflow.create, update and delete act only on workflows created at runtime. A name the configuration defines is refused, naming where it is defined (workflow.refused); change it in the configuration and reload. A runtime definition may not carry a tool: block, and may open an inbound route (a webhook start or a wait {on: webhook}) only when the operator grants interface in agent.document_capabilities.
  • When the configuration, after a reload or a restart, defines a name a runtime definition uses, the stored definition is discarded (workflow.stored.shadowed).

Over A2A, the command extension serves these ops for workflows; the A2A extensions page has the full table:

OpWho may callDoes
workflow.runuser, agent, or a grantstarts a workflow; the reply is the task it runs under
workflow.statususer, agent, or a grantone run's status, or every run the caller started
workflow.canceluser, or a grantcancels one run
workflow.signala grantdelivers a signal
run.getuser, or a grant; needs a2a.introspection.enabledone run with per-step status, timings, errors and output
admin.pause, admin.resumeoperatorholds or releases the instance or one run
admin.canceloperatorcancels any run

A workflow whose a2a start declares a command adds that command as one more op.

Next steps