> ## Documentation Index
> Fetch the complete documentation index at: https://docs.heylua.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Author a workflow

> Lay out a workflow file, bind data between steps, branch on a predicate, and set retries and budgets that survive a restart

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](/reference/sdk/workflow-builder); for a first run end to end, follow the [quickstart](/build/workflows/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.

<Steps>
  <Step title="Lay out the file">
    Put one [workflow](/concepts/workflows) 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`.

    ```ts src/workflows/ticket-followup.ts theme={null}
    import { z } from 'zod';
    import { createStep, createWorkflow, fromInit, fromStep, template, step, eq, lit, Data } from 'lua-cli';

    const Ticket = z.object({ id: z.string(), priority: z.enum(['low', 'high']), text: z.string() });
    const Summary = z.object({ summary: z.string() });

    const loadTicket = createStep({
      id: 'loadTicket',
      inputSchema: z.object({ ticketId: z.string() }),
      outputSchema: Ticket,
      timeoutSeconds: 60,
      retry: { maxAttempts: 3, backoffSeconds: 5, backoff: 'exponential' },
      async execute({ inputData }) {
        const res = await fetch(`https://api.example.com/tickets/${inputData.ticketId}`);
        if (!res.ok) throw new Error(`tickets api ${res.status}`);
        return (await res.json()) as z.infer<typeof Ticket>;
      },
    });

    const record = createStep({
      id: 'record',
      inputSchema: z.object({ ticketId: z.string(), summary: z.string(), runId: z.string() }),
      outputSchema: z.object({ recorded: z.boolean() }),
      async execute({ inputData, once }) {
        // A retry of this step must not create a second entry.
        await once('record', async () => (await Data.create('ticket-summaries', inputData)).id);
        return { recorded: true };
      },
    });

    export const ticketFollowup = createWorkflow({
      name: 'ticket-followup',
      description: 'Summarise a ticket, escalate high-priority ones, record the result.',
      inputSchema: z.object({ ticketId: z.string() }),
      outputSchema: z.object({ recorded: z.boolean() }),
      budget: { maxCredits: 5, maxDurationSeconds: 3600 },
    })
      .then(loadTicket)
      .agentStep('summarise', {
        agentId: '$self',
        toolScope: {},
        prompt: template('Summarise this ticket in two sentences: ${stepResults.loadTicket.text}'),
        outputSchema: Summary,
      })
      .switch([[eq(step(loadTicket).path('priority'), lit('high')), 'escalate']])
      .agentStep('escalate', {
        agentId: '$self',
        toolScope: {},
        prompt: template('Write an escalation note for ticket ${initData.ticketId}: ${stepResults.summarise.summary}'),
      })
      .map(
        {
          ticketId: fromInit('ticketId'),
          summary: fromStep('summarise', 'summary'),
          runId: template('${requestContext.runId}'),
        },
        { id: 'shape' }
      )
      .then(record)
      .commit();
    ```

    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`.
  </Step>

  <Step title="List it on the agent">
    Only workflows listed on the `LuaAgent` are compiled.

    ```ts src/index.ts highlight={2,7} theme={null}
    import { LuaAgent } from 'lua-cli';
    import { ticketFollowup } from './workflows/ticket-followup';

    export default new LuaAgent({
      name: 'support-agent',
      persona: 'You help the support team triage tickets.',
      workflows: [ticketFollowup],
    });
    ```
  </Step>

  <Step title="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).
  </Step>

  <Step title="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.
  </Step>

  <Step title="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](/build/workflows/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.
  </Step>

  <Step title="Verify">
    Compile, then run both switch arms offline by supplying the step the predicate reads.

    ```bash theme={null}
    lua compile --ci
    lua test --ci workflow --name ticket-followup --input '{"ticketId":"TCK-2"}' \
      --step-output 'loadTicket={"id":"TCK-2","priority":"low","text":"Typo on the pricing page"}'
    ```

    ```text Output theme={null}
    🧭 Running workflow locally: ticket-followup
    [17:53:52] run local-1789235632204 · 7 planned step(s)
    [17:53:52] loadTicket · step-output supplied — execute skipped
    [17:53:52] loadTicket · completed
    [17:53:52] summarise · completed
    [17:53:52] conditional@2 · completed
    [17:53:52] escalate · skipped
    [17:53:52] conditional@2.join · completed
    [17:53:52] shape · completed
    [17:53:52] record · completed
    [17:53:52] run · completed
    …
    ```

    With `"priority":"high"` the `escalate` line reads `completed`. The other flags are in [Test a workflow offline](/build/workflows/test-offline).
  </Step>
</Steps>

## 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](/build/workflows/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:

```text Output theme={null}
🔨 Compiling...❌ Compilation failed:
   src/workflows/broken.ts:13 - closure-predicate switch()/branch() arm 0: a closure was given where a LuaPredicate is expected — compute the condition inside a createStep and reference its output with step(x).path(…)
…
✖ compile_failed: Compilation failed — see errors above.
```

Every code is in the reference's [build errors table](/reference/sdk/workflow-builder#build-errors).

<Accordion title="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.
</Accordion>

<Accordion title="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.
</Accordion>

<Accordion title="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`.
</Accordion>

## Next steps

<Columns cols={2}>
  <Card title="Workflow builder reference" href="/reference/sdk/workflow-builder">Every chain method, option, binding helper, and build error.</Card>
  <Card title="Add approvals and signals" href="/build/workflows/approvals-and-signals">Pause a run for a person or an external event.</Card>
  <Card title="Test a workflow offline" href="/build/workflows/test-offline">Force branches, simulate faults, and gate CI.</Card>
  <Card title="Declare connections" href="/build/workflows/connections">Name the integrations a workflow acts through.</Card>
</Columns>
