- 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 (an agent step is one credit, a Job-tier attempt four), maxSteps, and maxDurationSeconds from 60 to 2,592,000. 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
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.

