- A project created with
lua initand signed in withlua auth configure. lua compile --cipasses before you add the workflow.
1
Lay out the file
Put one workflow per file under Schemas are zod: the platform validates the run input on every start and each step’s output after every attempt. Module top level must be pure; reads of the clock, the environment, or the network belong inside
src/workflows/: the createStep() objects first, then one createWorkflow({ … }) chain that ends in .commit(), exported as a const. The compiler recognizes the chain only when the config is an inline object literal with a literal name.src/workflows/ticket-followup.ts
execute.2
List it on the agent
Only workflows listed on the
LuaAgent are compiled.src/index.ts
3
Bind data between steps
.then(step) hands the previous entry’s output to the step. When the next step needs another shape, put a .map() in front of it: fromInit('path') reads the run input, fromStep(step, 'path') an upstream output, value(v) a literal, and template('…') renders a string at dispatch. Templates have four namespaces, ${initData.*}, ${stepResults.<id>.*}, ${state.*}, and ${requestContext.*} (runId, userId, trigger, threadId, and the rest of the run’s identity).4
Branch on a predicate
Predicates are data built from
step(x).path('…'), init('…'), state('…'), lit(v), and the comparators eq, ne, gt, gte, lt, lte, inSet, exists, truthy, and, or, not. The switch in the file names its arm as a string, and the escalate declared on the next line is placed inside that arm, because a container claims any entry declared elsewhere in the chain by id. A createStep object has no id to claim, so pass the object itself. Without otherwise, a false predicate continues past the switch.5
Set retries, timeouts, and a budget
retry is an engine-side policy: maxAttempts from 1 to 20, backoffSeconds between attempts, and 'exponential' doubling up to maxBackoffSeconds (3,600 by default). timeoutSeconds on the worker tier is at most 600, defaulting to 300 for a code step and 600 for an agent step; longer work moves to the Job tier. Execution is at-least-once, so wrap every external effect in ctx.once(key, fn), which replays the first result on a retry or resume. For a step that moves money or opens a pull request, add sideEffects: 'external', which stops the platform from re-running it after a fault, and onError: 'park', which parks it for a person instead.budget caps the run: maxCredits, maxSteps, and maxDurationSeconds from 60 to 2,592,000. What maxCredits counts depends on how your organization is billed and how the run is metered, and it caps this run, not your account balance; see what the budget counts. A graph with an approval, a signal wait, or a suspendable step defaults to 30 days, and lua compile warns hitl-duration-defaulted until you set it.6
Verify
Compile, then run both switch arms offline by supplying the step the predicate reads.With
Output
"priority":"high" the escalate line reads completed. The other flags are in Test a workflow offline.Options you may need
Choose a model per step
Every option in this section is optional, and leaving them all unset is a complete answer. A step with neithertaskClass nor model runs on the owning agent’s model, exactly as it did before these options existed; workflows you have already pushed are unaffected and nothing about them is rewritten.
On a workflow you push, name the class yourself. A step that carries a taskClass and no model is refused with task-class-without-model, because nothing maps a task class to a model when the step runs; the mapping happens while the agent composes a workflow from chat, not at dispatch. Keep the taskClass beside the class anyway — it is what the platform, the run card, and the logs group the step by.
For a step whose work is clearly lighter or heavier than the rest, ask for the class and say why in one line:
TypeScript
'classify' | 'extract' | 'transform' | 'draft' | 'research' | 'reason' | 'code' | 'judge'
What the step does. It maps to a model class —
classify, extract and transform to fast; draft, research and judge to balanced; reason and code to strong. On a pushed definition it needs a model beside it; the agent applies the map for you when it composes a workflow.string
A model code as before, or
'class/fast', 'class/balanced' or 'class/strong'. A class is resolved to one approved model when the step runs, so the step survives a model being retired.('vision' | 'structured' | 'largeContext' | 'codeExecution')[]
Traits the chosen model must have. Prefer this to pinning a code when what you actually need is a capability: a step that reads images needs
vision, a step with an outputSchema needs structured.'low' | 'medium' | 'high'
How hard the chosen model would think on this step.
low for light work and for tool-heavy orchestration, high for reason and code, medium otherwise. A model whose reasoning is a plain on/off toggle ignores it, and lua models list --workflows says which models those are.string
One line, at most 160 characters, saying why this step asks for what it asks for. Plain text on the run card, never parsed. A composed step that carries no reason gets the platform’s own —
draft step → balanced per platform map — with the ceiling appended when your organization’s class ceiling lowered the class.A plain push never sends
effort to the model: the platform validates it, records it on the step, counts it in the run estimate, and dispatches the step without it. lua push workflow --apply-effort marks the version so that, from that version on, each agent step’s effort is sent to the model where the platform forwards it; runs in flight and earlier versions keep their own marker, and a push without the flag goes back to estimate-only. lua workflows view shows an effort that is recorded but not sent as <effort> (not applied), and the step row’s effortApplied records what reached the model, null when it was dropped.lua workflows logs <runId> names the model each agent step resolved to and the class it asked for:
Output
lua workflows policy models get prints the settings in force and lua workflows policy models set changes them; reading needs access to the organization, changing needs an organization administrator, and --compose off turns assignment off altogether, leaving every composed step on the agent’s own model. Every flag is on the lua workflows reference. A workflow the agent composed before model classes existed keeps the codes it pinned until lua workflows recompose <name> rewrites them into class/… requests as a new version.
Pinning a code your organization excludes is a warning, not a refusal: the step pushes, the run starts, and the platform records a policy warning against the step — see Operate runs. Refusing such a pin at push may be enabled for your organization.
Per-environment values
env.template('KEY') stands wherever a template does and is resolved from the target environment at push; keys ending in SECRET, TOKEN, KEY, or PASSWORD are refused with env-template-secret-key. See Declare connections.
The script form
A workflow can also be a plain JavaScript module atsrc/workflows/<name>.workflow.script.js. Its first statement is export const meta, a literal of at most 4,096 bytes whose name equals the file stem (^[a-z][a-z0-9-]{0,63}$) and whose description is 1 to 500 characters; the body runs as top-level await over the host bindings args, agent, tool, workflow, parallel, foreach, sleep, sleepUntil, approval, waitForSignal, step, memo, shell, merge, log, phase, bailRun, artefacts, and the deterministic now(), random(), and uuid(), and a top-level return is the run output. Any other import or export is SCRIPT_IMPORT_FORBIDDEN; Date.now(), new Date(), Math.random(), globalThis, eval, and new Function are SCRIPT_NONDETERMINISM; over 256 KB is SCRIPT_TOO_LARGE.
If it isn’t working
A builder refusal surfaces whenlua compile evaluates the module, naming the file, the code, and the fix:
Output
closure-predicate and closure-binding
closure-predicate and closure-binding
A function was passed where a predicate, template, or mapping is expected. Compute the value in a code step and reference its output with
step(x).path('…'), or build the binding with a descriptor helper.unknown-step-ref and duplicate-step-id
unknown-step-ref and duplicate-step-id
A string arm names nothing declared in the chain, or an id is declared or claimed twice. Declare each entry once and pass
createStep objects as objects.WORKFLOW_UNPLACED_STEP and map-id-required
WORKFLOW_UNPLACED_STEP and map-id-required
Both are compile warnings.
lua push workflow refuses a createStep object that no chain places; place it or delete it. Two or more maps without ids renumber when you insert one, so give every map an id.Next steps
Workflow builder reference
Every chain method, option, binding helper, and build error.
Add approvals and signals
Pause a run for a person or an external event.
Test a workflow offline
Force branches, simulate faults, and gate CI.
Declare connections
Name the integrations a workflow acts through.
Models
Model classes, effort, and the words around them.

