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

# PreProcessor

> Code that runs on each end-user message before the model and can rewrite it or block the turn

`PreProcessor` runs your code on the end user's messages before the model sees them, and returns whether the turn proceeds, proceeds with different messages, or stops with a reply of its own. Register instances on [`LuaAgent`](/reference/sdk/luaagent) under `preProcessors`. Inside `execute`, the runtime objects (`User`, `Data`, `Lua`, `env`) are available as in a tool. For where a preprocessor sits in a turn, see [processors](/concepts/processors).

*Verified against lua-cli 3.33.0.*

```ts theme={null}
import { PreProcessor } from 'lua-cli';
```

## Quick example

A preprocessor that blocks a refund request until it carries an order number:

```ts src/preprocessors/OrderIdGuard.ts theme={null}
import { PreProcessor } from 'lua-cli';

export default new PreProcessor({
  name: 'order-id-guard',
  description: 'Ask for an order number before the model sees a refund request',
  priority: 10,
  async execute(user, messages, channel) {
    const text = messages
      .filter((m) => m.type === 'text')
      .map((m) => m.text)
      .join('\n');
    if (/refund/i.test(text) && !/\b\d{6,}\b/.test(text)) {
      return { action: 'block', response: 'Please include your order number so I can look up the refund.' };
    }
    return { action: 'proceed' };
  },
});
```

## Constructor

Creates a preprocessor from a `PreProcessorConfig`. The class is also exported under the alias `LuaPreprocessor`.

```ts theme={null}
new PreProcessor(config: PreProcessorConfig): PreProcessor
```

**Errors** — `PreProcessor requires a non-empty \`name\` (used as the server-side identifier).`when`name\` is missing or blank.

## Configuration

<ParamField path="name" type="string" required>
  Server-side identifier, and the name `lua push preprocessor --name` and `lua preprocessors` address. Kebab-case, for example `order-id-guard`.
</ParamField>

<ParamField path="description" type="string" required>
  Short description shown in listings. Not seen by the model.
</ParamField>

<ParamField path="execute" type="(user, messages, channel) => Promise<PreProcessorResult>" required>
  Called once per turn with the end user's [`UserDataInstance`](/reference/sdk/user), the turn's `ChatMessage[]`, and the channel identifier (`'web'`, `'whatsapp'`, `'email'`, …). Must resolve to a `PreProcessorResult`.
</ParamField>

<ParamField path="priority" type="number" default={100}>
  Execution order. Lower numbers run first; preprocessors with the same priority run in the order the server stores them, not the order of the `preProcessors` array.
</ParamField>

<ParamField path="async" type="boolean" default={false}>
  Stored with the pushed version. The deployed runtime runs every preprocessor in sequence and waits for each result regardless of this flag.
</ParamField>

## Result

`execute` returns one of two shapes; there is no `'allow'` or `'modify'` action.

```ts theme={null}
type PreProcessorResult =
  | { action: 'proceed'; modifiedMessage?: ChatMessage[]; metadata?: Record<string, any> }
  | { action: 'block'; response: string; metadata?: Record<string, any> };
```

### Proceed

`{ action: 'proceed' }` hands the current messages to the next preprocessor, or to the model when this is the last one. Set `modifiedMessage` to replace them; the replacement is what every later preprocessor and the model receive.

### Block

`{ action: 'block', response }` stops the chain. The model is not called, later preprocessors do not run, and `response` is sent to the end user as the agent's reply.

### Empty response

`{ action: 'block', response: '' }` stops the chain, but what the end user sees depends on the path the turn came in on. The web widget and other UI-stream channels show the default text `User not eligible for agent response`; a caller of the generate endpoint or [`Agents.invoke`](/reference/sdk/agents) receives a `preprocessor_blocked` result with empty text; only the legacy streaming path sends no text. There is no channel-independent way to block silently.

## Message types

`messages` is a `ChatMessage[]`; every member carries a `type` discriminator.

```ts theme={null}
type ChatMessage = TextMessage | ImageMessage | FileMessage;

interface TextMessage  { type: 'text';  text: string }
interface ImageMessage { type: 'image'; image: string; mediaType: string }
interface FileMessage  { type: 'file';  data: string;  mediaType: string }
```

`image` and `data` hold a URL or base64 content; `mediaType` is the MIME type, for example `image/png` or `application/pdf`.

## Instance methods

| Method                             | Returns                                                                   |
| ---------------------------------- | ------------------------------------------------------------------------- |
| `getName()`                        | `string`                                                                  |
| `getDescription()`                 | `string`                                                                  |
| `getPriority()`                    | `number`, resolved default `100`                                          |
| `getAsync()`                       | `boolean`, resolved default `false`                                       |
| `execute(user, messages, channel)` | `Promise<PreProcessorResult>`; runs your handler directly, for unit tests |

## Execution order

* Only preprocessors that are pushed and active run (`lua preprocessors activate`). They run in ascending `priority`, one at a time; each receives the messages the previous one returned.
* The first `block` ends the chain. A `proceed` without `modifiedMessage` passes the current messages through unchanged.
* A preprocessor that throws or times out is logged (`lua logs --type preprocessor`) and skipped, and the chain continues with the current messages. On a voice channel the same failure blocks the turn instead, with the spoken reply `Sorry, I'm having trouble right now. Could you try that again?` and `metadata: { failedPreprocessor: '<name>', failClosed: true }`.
* `execute` has 180 seconds to return.
* `lua chat` in the sandbox sends a `PreProcessorOverride` for every preprocessor listed in `lua.skill.yaml`, so the sandbox turn runs your sandbox version in place of the production one. You never construct overrides yourself.

## CLI commands

| Task                  | Command                                                                          |
| --------------------- | -------------------------------------------------------------------------------- |
| Run `execute` locally | `lua test preprocessor --name <name> --input '{"message":"hi","channel":"web"}'` |
| Push a version        | `lua push preprocessor --name <name> --set-version <ver>`                        |
| Make a version live   | `lua deploy preprocessor --name <name> --set-version latest --force`             |
| Turn on or off        | `lua preprocessors activate --preprocessor-name <name>`, `… deactivate …`        |

The [`lua test`](/reference/cli/test) input object accepts `message` and `channel`; the message is wrapped as a single `TextMessage`.

## Types

All of these are exported from `'lua-cli'`.

<ResponseField name="PreProcessorConfig" type="interface">
  `{ name; description; async?; priority?; execute }` as documented under Configuration.
</ResponseField>

<ResponseField name="PreProcessorResult" type="PreProcessorBlockResponse | PreProcessorProceedResponse">
  The union `execute` returns. `PreProcessorAction` is `'proceed' | 'block'`.
</ResponseField>

<ResponseField name="PreProcessorBlockResponse" type="type">
  `{ action: 'block'; response: string; metadata?: Record<string, any> }`.
</ResponseField>

<ResponseField name="PreProcessorProceedResponse" type="type">
  `{ action: 'proceed'; modifiedMessage?: ChatMessage[]; metadata?: Record<string, any> }`.
</ResponseField>

<ResponseField name="ChatMessage, TextMessage, ImageMessage, FileMessage" type="type">
  The message shapes under Message types.
</ResponseField>

<ResponseField name="PreProcessorOverride" type="interface">
  `{ preprocessorId: string; sandboxId: string }`. Sent by `lua chat` to run a sandbox version.
</ResponseField>

<ResponseField name="LuaPreprocessor" type="alias">
  The same class as `PreProcessor`.
</ResponseField>

## See also

* [`PostProcessor`](/reference/sdk/postprocessor) — the same chain on the model's reply
* [Processors](/concepts/processors) — order, blocking, and processor versus skill
* [Add a processor](/build/add-a-processor) — how-to
* [`User`](/reference/sdk/user) — the `user` argument and its methods
* [`lua preprocessors`](/reference/cli/processors) — activate, deactivate, versions
