Skip to main content

Overview

A workflow is written with two functions from lua-cli:
  • createStep({...}) declares a code step: typed input and output plus an execute function.
  • 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: [...].
The graph must be static. Predicates and bindings are built with the helpers below - a function where a predicate, a template or a mapping is expected fails the build with closure-predicate / closure-binding. Anything that must be computed goes inside a step’s execute.

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 by agentStep / specialistStep / toolStep / map(..., { id }) anywhere else in the same chain, and places that step inside the container:
If a conditional is the last entry, the taken arm’s output is the run output.

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
Objects and arrays are JSON-encoded inside the prompt; a missing value is left verbatim. Prompt strings are plain single-quoted strings so TypeScript never interpolates them.

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:
Approvals and signal waits are top-level entries - they cannot sit inside parallel, foreach or a loop.

Signals

A waitForSignal 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:
A signal that arrives before the run waits for it is parked and consumed when the wait begins (each run holds at most 256 parked signals; payloads are at most 64 KB). 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

The child is a run of its own (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

A LuaTrigger can start a run by returning startWorkflow from transform:

Budgets and policy hooks

  • budget.maxCredits, maxSteps and maxDurationSeconds on the definition are the per-run defaults; Workflows.start and lua workflows start --budget-credits can lower or override them. When a dimension runs out the run parks on a budget gate and can be raised (raiseBudget, or the desktop).
  • concurrencyPolicy: 'forbid' is the overlap guard for scheduled workflows.
  • sideEffects: 'external' plus onError: '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.
  • outputVisibility restricts who can read a workflow’s outputs; readers without access see restricted: true.
  • toolScope on 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:
A missing key aborts the push before anything is sent. Keys that look like secrets (ending in 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 in src/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.