Skip to main content
After this guide, your workflow file compiles, each step’s input is bound from earlier outputs, one arm runs only when a predicate holds, and a retry never repeats a side effect. Every method and option is in the workflow builder reference; for a first run end to end, follow the quickstart. Verified against lua-cli 3.33.0. Before you begin
  • A project created with lua init and signed in with lua auth configure.
  • lua compile --ci passes before you add the workflow.
1

Lay out the file

Put one workflow per file under 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
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 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.
Output
With "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 at src/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 when lua compile evaluates the module, naming the file, the code, and the fix:
Output
Every code is in the reference’s build errors table.
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.
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.
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.