> ## 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.

# Test a workflow offline

> Run a workflow's graph on your machine with faked agent steps and pre-answered approvals, then gate CI on it

After this guide, you run a workflow end to end on your machine, force each branch of its graph, and fail a CI job when a step breaks. Use it before every `lua push workflow`; for the real model and real approvers, [start a run on the platform](/build/workflows/operate-runs) instead.

*Verified against lua-cli 3.33.0.*

**Before you begin**

* A project with a [workflow](/concepts/workflows) listed under `workflows` on its `LuaAgent`. Only listed workflows are compiled.
* Signed in with `lua auth configure`. Code steps run with your credential, so a step that calls `Data`, `AI`, or `Channels` reaches the platform.

<Steps>
  <Step title="Run the graph with every decision answered">
    `lua test workflow` compiles the project without registering anything on the server and drives the graph in one process. Code steps run for real, agent steps return a fake reply, and approvals are answered from flags.

    ```bash theme={null}
    lua test --ci workflow --name lead-outreach --input '{"leadId":"lead_abc123"}' --approve reviewDraft
    ```

    ```text Output theme={null}
    📦 Compiling code first...
    🔨 Compiling...🔄 Syncing YAML with manifest...ℹ️  Server sync skipped — nothing was sent to the server; `lua push` publishes.
    ✅ Compiled 4 primitives (1 agent, 1 trigger, 1 job, 1 workflow) in 540ms

    🧭 Running workflow locally: lead-outreach
    [17:48:11] run local-1789235291603 · 4 planned step(s)
    [17:48:11] loadLead · log · loading lead_abc123
    [17:48:11] loadLead · completed
    [17:48:11] draftEmail · completed
    [17:48:11] reviewDraft · completed
    [17:48:11] recordSend · completed
    [17:48:11] run · completed

    Workflow returned: Object — fields: sent
    Output:
    { sent: true }
    …
    ```

    An agent step with an `outputSchema` returns an object shaped from the schema (strings `''`, numbers `0`, booleans `false`); without one it returns `{ text: '[fake:<id>] <prompt>' }`. `--deny <id[=@reason]>` answers an approval with `approved: false` and `--signal <name=json>` supplies a `waitForSignal` payload. Tool steps and nested workflows cannot run offline: give them an output with `--step-output`.
  </Step>

  <Step title="Force a branch with --step-output">
    A fake reply never satisfies a predicate, so complete the step the predicate reads with the output you want; the value is validated against the step's `outputSchema` before anything runs.

    ```bash theme={null}
    lua test --ci workflow --name ticket-followup --input '{"ticketId":"TCK-1"}' \
      --step-output 'loadTicket={"id":"TCK-1","priority":"high","text":"Checkout fails on Safari"}'
    ```

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

    With `"priority":"low"` the `escalate` line reads `skipped`. A value that does not match the schema stops before the run starts, with exit 2 and a line such as `step-output-schema --step-output draftEmail: $.subject: required; $.body: required; $.nope: not allowed`. The flag repeats and takes `@file`.
  </Step>

  <Step title="Simulate a fault, a retry, and a clock">
    `--park <id>` stands in for a platform fault on that step. Offline the run cannot wait for a person, so it fails with exit 4 after printing what the platform would do.

    ```bash theme={null}
    lua test --ci workflow --name lead-outreach --input '{"leadId":"lead_abc123"}' --deny reviewDraft --park loadLead
    ```

    ```text Output theme={null}
    🧭 Running workflow locally: lead-outreach
    [17:49:45] run local-1789235385061 · 4 planned step(s)
    [park:loadLead] EFFECT_IN_DOUBT — the run would park here (lua workflows retry-step, or a desktop decision); offline it fails
    [17:49:45] loadLead · failed · EFFECT_IN_DOUBT: --park loadLead: simulated platform-fault reclaim
    [17:49:45] draftEmail · skipped
    [17:49:45] reviewDraft · skipped
    [17:49:45] recordSend · skipped
    [17:49:45] run · failed · EFFECT_IN_DOUBT: --park loadLead: simulated platform-fault reclaim

    ❌ Workflow failed: EFFECT_IN_DOUBT --park loadLead: simulated platform-fault reclaim
       at step "loadLead"
    ```

    On a terminal the driver offers `retry`, `skip`, `complete`, or `fail` instead. A step that throws is retried under its `retry` policy and prints `<id> · retry · attempt 2/3`; `--fast-retries` collapses the backoff waits. Sleeps and deadlines run on a virtual clock, and `--now <iso>` sets its start, so a business-hours escalation needs no waiting.
  </Step>

  <Step title="Seed completed steps from a platform run">
    To re-drive only what did not finish, seed the ledger from a platform run: each completed step whose upstream steps are also seeded keeps its output and `once()` effects, and the driver starts at the first pending step.

    ```bash theme={null}
    lua test --ci workflow --name lead-outreach --input @lead.json --approve reviewDraft --from-run <runId>
    ```

    The driver prints `[from-run] <n> step(s) seeded from <runId>` before the timeline. A run that does not exist exits 3.
  </Step>

  <Step title="Record real agent replies and replay them">
    `--agents live` sends each agent step to your agent through the platform. `--record` writes each live reply as a fixture (tool steps never run offline, so nothing else is recorded); `--fixtures` replays them without a model in the loop.

    ```bash theme={null}
    lua test --ci workflow --name lead-outreach --input @lead.json --approve reviewDraft --agents live --record ./fixtures
    lua test --ci workflow --name lead-outreach --input @lead.json --approve reviewDraft --fixtures ./fixtures
    ```

    A fixture is `<dir>/<stepId>.<attempt>.json` holding `{ input, output, ms }`; only `output` is replayed, and a step without one fails the run with exit 5:

    ```text Output theme={null}
    ❌ Workflow failed: FIXTURE_MISSING FIXTURE_MISSING: no fixture for step "draftEmail" attempt 1 (fixtures/draftEmail.1.json)
       at step "draftEmail"
    ```
  </Step>

  <Step title="Verify in CI">
    In CI, stdin is not a terminal, so anything left unanswered ends the run with exit 2 and a line such as `APPROVAL_UNANSWERED approval "reviewDraft" needs --approve/--deny reviewDraft (no interactive stdin)`. `--ci` makes a prompt on a developer's terminal fail instead of waiting.

    ```bash theme={null}
    set -e
    lua compile --ci
    lua test --ci workflow --name lead-outreach --input @test/lead.json --approve reviewDraft \
      --step-output 'draftEmail={"subject":"Quick intro","body":"Hello Ada"}' --json --ledger-out ledger.json > approved.json
    lua test --ci workflow --name lead-outreach --input @test/lead.json --deny reviewDraft --json > denied.json
    ```

    `--json` prints one document on stdout, `{ "success": true, "data": { "status", "output", "exitCode", "ledger" } }`, sends the compile lines to stderr, and prints no timeline. The exit code is the verdict:

    | Code | When                                                                                                 |
    | ---- | ---------------------------------------------------------------------------------------------------- |
    | `0`  | The run completed                                                                                    |
    | `2`  | A flag, input, or `--step-output` failed validation; an approval, signal, or resume was not answered |
    | `3`  | No workflow with that name; no workflows compiled; `--from-run` run not found                        |
    | `4`  | A step failed past its retries, or the run output failed `outputSchema`                              |
    | `5`  | A fixture is missing under `--fixtures`                                                              |
  </Step>
</Steps>

## Options you may need

### `lua workflows run` and `--env`

`lua workflows run <name>` is the same driver with every flag on this page plus `--env KEY=value`, a local overlay for `env.template('KEY')` placeholders; a placeholder with no value exits 2 with `env-template-missing`, and the values never reach the ledger.

### Job-tier steps

A step with `tier: 'job'` gets `ctx.workspace`, `ctx.exec`, and `ctx.$` only with `--workspace <dir>`, the checkout to run in; `--job-wall <s>` fakes a long coding turn as 14,400-second segments. See [Use the Job tier](/build/workflows/job-tier).

### The ledger

`--ledger-out <file>` writes the run as JSON: `runId`, `workflowId`, `graphHash`, `status`, `input`, `steps`, `state`, `effects` (settled `once()` results), `events`, and `output`. `--artefacts-dir <dir>` backs `ctx.artefacts` on disk; without it, `put` and `get` throw.

## If it isn't working

<Accordion title="TOOL_NOT_AVAILABLE_OFFLINE">
  A `toolStep` has no offline executor. Supply its output with `--step-output <id>=<json>` or a fixture; a nested `.workflow()` step reports `SUBRUN_NOT_SUPPORTED_OFFLINE` and takes the same fix.
</Accordion>

<Accordion title="EXEC_UNAVAILABLE">
  A Job-tier step called `ctx.exec` or `ctx.$` without `--workspace <dir>`. Pass the directory the step should run in.
</Accordion>

<Accordion title="RESUME_UNANSWERED">
  A step called `ctx.suspend()` and no terminal was attached to type its `resumeData`. Run the command in a terminal without `--ci`, or complete the step with `--step-output <id>=<json>`.
</Accordion>

## Next steps

<Columns cols={2}>
  <Card title="Operate runs" href="/build/workflows/operate-runs">Start, watch, approve, and repair runs on the platform.</Card>
  <Card title="lua test reference" href="/reference/cli/test">Every workflow flag and its default.</Card>
  <Card title="Automate releases in CI" href="/ship/ci-and-automation">Exit codes, `--ci`, and the JSON envelope across the CLI.</Card>
  <Card title="Author a workflow" href="/build/workflows/authoring">Bindings, predicates, retries, and the script form.</Card>
</Columns>
