> ## 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 an agent before you release

> Run tools, webhooks, jobs, processors, workflows, and voice tests locally, then confirm the agent in the sandbox and in production

After this guide, you can prove a change works at every rung of the ladder: the function alone, the agent in the [sandbox](/concepts/environments), and the live agent after a release. Each rung is one command that needs no prompt, so it runs the same from your shell, a script, or a coding agent. For the release itself, see [Release an agent to production](/ship/releasing).

*Verified against lua-cli 3.33.0.*

**Before you begin**

* A project created with `lua init` and signed in with `lua auth configure` ([Install and sign in](/get-started/install)). Local runs still need a credential and the project's agent ID.
* Sandbox secrets in `.env`, set with `lua env sandbox -k <KEY> -v <value>`. `lua test` loads `.env` into the run; see the [`lua env` reference](/reference/cli/env).

<Steps>
  <Step title="Run one tool with exact input">
    `lua test skill` compiles the project, then calls the tool's `execute` function directly in a local runtime. No model is involved, so the output is exactly what the tool returns. `--name` is the [tool](/concepts/skills-and-tools) name, not the skill name, and `--input` is the tool's own input fields.

    ```bash theme={null}
    lua test skill --name lookup_tickets --input '{"customerEmail":"user@example.com"}'
    ```

    ```text Output theme={null}
    …
    ✅ Compiled 8 primitives (1 agent, 1 skill, 3 tools, 1 webhook, 1 preprocessor, 1 postprocessor) in 641ms
    ✅ Selected tool: lookup_tickets
    Input: {
      "customerEmail": "user@example.com"
    }
    ✅ Tool execution successful!

    Tool returned: Array[0] of unknown
    Output:
    []
    ```

    Platform calls inside the tool (`Data`, `User`, `Channels`) go to the real API under your credential. A tool that throws does not fail the command: the run still prints `✅ Tool execution successful!`, exits 0, and shows `{ status: 'error', error: '<message>' }` as the output, so check the output rather than the exit code (with `--json`: `jq -e 'type != "object" or .status != "error"'`). A run that ends with `Tool "<name>" not found` means you passed the skill name; run `lua test skill` with no flags to pick from the compiled tool list.
  </Step>

  <Step title="Run a webhook, job, or processor the same way">
    The other `lua test` types take the shape their primitive receives. A webhook gets `query`, `headers`, and `body`; a job takes no input; a preprocessor gets `message` and `channel`; a postprocessor gets `message`, `response`, and `channel`.

    ```bash theme={null}
    lua test webhook --name ticket-status-webhook \
      --input '{"query":{},"headers":{},"body":{"ticketId":"TCK-0000000000-000000","status":"closed"}}'
    lua test job --name <job-name>
    lua test preprocessor --name pii-redaction \
      --input '{"message":"Call me on +1 555 123 4567 or mail jane@example.com","channel":"web"}'
    lua test postprocessor --name ticket-footer \
      --input '{"message":"hi","response":"Your ticket is open.","channel":"whatsapp"}'
    ```

    ```text Output theme={null}
    …
    ✅ Webhook execution successful!

    Webhook returned: Object — fields: ok, error, ticketId
    Output:
    { ok: false, error: 'ticket not found', ticketId: 'TCK-0000000000-000000' }
    ```

    A preprocessor run prints the action and the processed messages (`Action: PROCEED`, then `1. [TEXT] Call me on [phone] or mail [email]`); a postprocessor run prints `Processed response:` followed by the modified text. The webhook's `secret` is not checked here: the platform verifies signatures only on incoming requests.
  </Step>

  <Step title="Talk to the agent in the sandbox">
    `lua chat -e sandbox` compiles your project, uploads the compiled skills and processors as sandbox versions, and sends your persona with each message; the platform runs them against the model, so no `lua push` is needed and production is untouched. Pass `-t` with no value to get a fresh, isolated [thread](/reference/cli/chat) whose ID is printed as `ℹ️  Thread: <id>`; add `--clear` to delete that history when the command exits.

    ```bash theme={null}
    lua chat -e sandbox -m "What is the status of my tickets? My email is user@example.com" -t --clear
    ```

    Without `-t`, the message lands in your default thread and earlier turns shape the answer. Without `-e`, `lua chat -m` defaults to the sandbox and says so. Sandbox chat reads the same `.env` as `lua test`: `lua env sandbox -k <KEY> -v <value>` writes only that file, there is no server-side sandbox variable store, and `lua chat -e sandbox` uploads the file's values with the sandbox versions it compiles, so your tools run on the platform with them. Production reads the variables set with `lua env production`.
  </Step>

  <Step title="Run a workflow offline">
    `lua test workflow` drives a [workflow](/concepts/workflows) locally on a virtual clock. Agent steps are faked by default (`--agents fake` answers `[fake:<stepId>] …` plus a schema-shaped object); tool steps need `--step-output <stepId>=<json>` or recorded `--fixtures <dir>`; approvals are pre-answered with `--approve <stepId>` or `--deny <stepId>`.

    ```bash theme={null}
    lua test workflow --name <workflow-name> --input '{"leadId":"lead_abc123"}' \
      --agents fake --approve reviewDraft --step-output sendEmail='{"sent":true}'
    ```

    Code steps are not faked: they execute your TypeScript, and platform calls inside them (`AI.generate`, `Channels.send`, `Data`) run for real against the API. Pass `--agents live` to call the model for agent steps too. The complete driver is on [Test a workflow offline](/build/workflows/test-offline).
  </Step>

  <Step title="Run voice tests">
    `lua voice test` runs every `*.voice.test.ts` file in the project with Jest or Vitest, whichever the project has installed; `--voice <name>` limits the run to `<name>.voice.test.ts`.

    ```bash theme={null}
    lua voice test --bail
    ```

    The command exits non-zero when a test fails or when neither runner is installed. Test helpers are described on the [`lua voice` reference](/reference/cli/voice).
  </Step>

  <Step title="Verify production after a release">
    Once a version is live, send one message to production on a fresh thread, then confirm that no `agent_error` entry and no `skill` entry with `subType` `error` were written; a throwing tool is logged under `skill`. The time-bounded form of this check is in [Release an agent to production](/ship/releasing).

    ```bash theme={null}
    lua chat -e production -m "What is the status of my tickets? My email is user@example.com" -t --clear
    lua logs --type agent_error --limit 5 --json
    lua logs --type skill --limit 20 --json | jq '[.logs[] | select(.subType == "error")]'
    ```

    ```text Output theme={null}
    {
      "logs": [],
      "pagination": {
        "currentPage": 1,
        "totalPages": 0,
        "totalCount": 0,
        "limit": 5,
        "hasNextPage": false,
        "hasPrevPage": false,
        "nextPage": null,
        "prevPage": null
      }
    }
    ```

    An empty `logs` array is the pass condition. Anything else: read the entries with [Read logs and debug an agent](/ship/logs-and-debugging).
  </Step>
</Steps>

## Options you may need

### Script the output

Add `--json` to any `lua test` type to get the raw return value on stdout with progress on stderr, so `lua test skill --name lookup_tickets --input '{"customerEmail":"user@example.com"}' --json | jq length` works. `lua logs --json` returns `{ logs, pagination }`.

### Let a coding agent run the ladder

The Claude Code plugin's `/lua-qa` command runs compile, `lua test`, and a sandbox conversation, then reports what failed; see [Claude Code plugin](/build-with-ai/claude-code-plugin).

## If it isn't working

<Accordion title="✖ not_found: Tool &#x22;<name>&#x22; not found">
  Exit code 3. `--name` received a skill name or a misspelled tool name. Tool names are the `name` property of the class, for example `lookup_tickets`; run `lua test skill` without flags to see the compiled list.
</Accordion>

<Accordion title="✖ usage: Invalid JSON input: …">
  Exit code 2. The `--input` string is not valid JSON. Wrap it in single quotes so the shell leaves the double quotes alone, and pass the tool's fields directly, without a `{"tool": …}` envelope.
</Accordion>

<Accordion title="✖ error: No supported test runner found.">
  `lua voice test` needs Jest or Vitest in the project. Install one (`npm install -D vitest`) and re-run; `--runner jest` or `--runner vitest` forces the choice.
</Accordion>

## Next steps

<Columns cols={2}>
  <Card title="Release an agent to production" href="/ship/releasing">Push, version, promote, verify, and roll back.</Card>
  <Card title="Read logs and debug an agent" href="/ship/logs-and-debugging">Filter `lua logs` and run the debug loop.</Card>
  <Card title="lua test reference" href="/reference/cli/test">Every type, flag, and exit code.</Card>
  <Card title="About environments" href="/concepts/environments">What the sandbox shares with production.</Card>
</Columns>
