Overview
A workflow is written with two functions fromlua-cli:
createStep({...})declares a code step: typed input and output plus anexecutefunction.createWorkflow({...})returns a builder; chain steps and containers on it and finish with.commit().
defineWorkflow(config, (wf) => wf.then(...).commit()) is equivalent sugar. Put workflows anywhere under src/ (the compiler finds every createWorkflow(...).commit() chain and every defineWorkflow call); src/workflows/ is the convention. Optionally list them on your LuaAgent under workflows: [...].
createWorkflow config
createStep
The step context
execute receives one object:
Execution is at-least-once: a step can run again after a crash, a retry, a resume or a repair run. Wrap anything with an external effect in
ctx.once(key, fn), or send your own idempotency key (for example refund:${ticketId}) when the effect must be unique across independent runs.The builder chain
Placement: declare inside the container
A string in a container refers to a step declared byagentStep / specialistStep / toolStep / map(..., { id }) anywhere else in the same chain, and places that step inside the container:
Data flow
Mapping helpers
Prompt bindings
template('...') strings are resolved by the engine when the step is dispatched:
${initData.<path>}- the run input${stepResults.<stepId>.<path>}- an upstream output${state.<key>}- the run state
Predicates
Build predicates from typed references and literals:Agent, specialist and tool steps
agentStep(id, opts)
specialistStep(id, opts)
Runs your own agent with an additive role - a name, instructions (at most 4000 characters) and a tool allowlist drawn from the agent’s own tools (at most 64; delegation tools cannot be allowlisted). Use it for a reviewer, a verifier or a planner without creating another agent:
role: { ref: 'reviewer' } points at a role from your organisation’s role library instead of an inline definition.
toolStep(id, tool, opts)
Calls a LuaTool directly with input built from mapping helpers, plus timeoutSeconds, retry, sideEffects, onError and requiredConnections.
Approvals
The card shows the previous step’s output as the payload. The approval’s own output is what the next step reads:
parallel, foreach or a loop.
Signals
AwaitForSignal step parks the run until something outside delivers a named signal:
The step’s output is
{ received: true, payload } (or the timed-out shape above). Deliver signals from a webhook with Workflows.signal:
dedupeKey makes a redelivered webhook a no-op. A signal that reaches a parent run is handed down to a waiting child run.
Nested workflows
trigger: 'workflow'), nested at most 3 deep. Reference another workflow by its LuaWorkflow value or by name on the same agent. workspace: 'inherit' runs the child on the parent’s Job-tier workspace.
Starting runs from code
Workflows is available in tools, jobs, webhooks, triggers and workflow code steps:
Idempotency. A second
start with the same idempotencyKey on the same agent returns the original run (idempotentReplay: true) instead of creating another. Keys are at most 128 characters. A concurrencyPolicy: 'forbid' workflow with a run in flight throws RUNS_IN_FLIGHT with blockingRunId.
From a code step, Workflows.start creates a detached run; use the .workflow() node when the parent should wait for the child.
From a trigger
ALuaTrigger can start a run by returning startWorkflow from transform:
Budgets and policy hooks
budget.maxCredits,maxStepsandmaxDurationSecondson the definition are the per-run defaults;Workflows.startandlua workflows start --budget-creditscan lower or override them. When a dimension runs out the run parks on abudgetgate and can be raised (raiseBudget, or the desktop).concurrencyPolicy: 'forbid'is the overlap guard for scheduled workflows.sideEffects: 'external'plusonError: 'park'is the pattern for money-moving or PR-opening steps: a platform fault never re-runs them and a final failure waits for a person.outputVisibilityrestricts who can read a workflow’s outputs; readers without access seerestricted: true.toolScopeon agent steps is mandatory when the prompt carries external content.
Per-environment values
env.template('KEY') is resolved from the target environment when you lua push workflow, so staging and production can route to different agents or timezones from one source file:
SECRET, TOKEN, KEY, PASSWORD) are refused - read those with env('KEY') inside execute instead. lua workflows env-overlay <name> shows which keys a version carries and whether each is present (values are never printed).
Build errors
commit() and lua compile refuse graphs that cannot run. The most common codes:
Script form
Workflows can also be written as a plain JavaScript script insrc/workflows/<name>.workflow.script.js that exports meta (name, description, phases, concurrency, sampleArgs) and uses agent(...), parallel(...), foreach(...), step(...) and log(...) as top-level awaits. lua push workflow and lua test workflow accept both forms; the desktop shows a Phases view instead of the graph for script runs.
Testing locally
lua test workflow --name <name> (or lua workflows run <name>) compiles the project and drives the graph offline. Agent steps are faked unless --agents live; every approval, input suspend and signal can be pre-answered:
Give every predicate both truth values: the fake agent stub alone cannot reach a
gt(confidence, 0.6) arm, so pass --step-output for the agent step that feeds it.
Related
- Workflows - the concept
- Job tier - workspaces, coding turns, size classes
- Runs and events - statuses, the ledger, SSE
- Workflows Command - the CLI reference

