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

# Add a processor

> Run your code on every message before the model and on every reply after it, test it locally, and release it

After this guide, every incoming message passes through your code before the model reads it, and every reply passes through your code before the end user sees it. Use a [processor](/concepts/processors) for a rule that must hold in every conversation; for a decision the model should make, write a [tool](/concepts/skills-and-tools).

*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)).

<Steps>
  <Step title="Write a preprocessor">
    `execute` receives the end user, the message as an array of parts, and the channel. Return `{ action: 'proceed' }`, optionally with `modifiedMessage` for the model to read instead, or `{ action: 'block', response }` to end the turn with `response` as the reply. Lower `priority` runs first; the default is 100. `channel` is the inbound name from [channels](/concepts/channels): `pop` for the web widget (the type lists it as `web`, so compare against both), `whatsapp`, `facebook` for Messenger, `email`, `sms`; `lua chat` sends `dev`. These differ from the `Channels.send` names (`webchat`, `messenger`).

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

    const CARD_NUMBER = /\b(?:\d[ -]?){13,19}\b/;
    const EMAIL = /[\w.+-]+@[\w-]+\.[\w.-]+/g;

    export default new PreProcessor({
      name: 'pii-guard',
      description: 'Refuse card numbers and hide email addresses before the model reads the message',
      priority: 10,
      async execute(_user, messages) {
        const texts = messages.filter((m) => m.type === 'text');
        if (texts.some((m) => CARD_NUMBER.test(m.text))) {
          return {
            action: 'block',
            response: 'Please don\'t share card numbers here. Use the secure payment link instead.',
          };
        }
        const modifiedMessage = messages.map((m) =>
          m.type === 'text' ? { ...m, text: m.text.replace(EMAIL, '[email]') } : m,
        );
        return { action: 'proceed', modifiedMessage };
      },
    });
    ```
  </Step>

  <Step title="Write a postprocessor">
    `execute` receives the end user, the original message, the model's reply, and the channel, and must return `{ modifiedResponse }`; that text replaces the reply.

    ```ts src/postprocessors/WhatsAppFooter.ts theme={null}
    import { PostProcessor } from 'lua-cli';

    export default new PostProcessor({
      name: 'whatsapp-footer',
      description: 'Add the opt-out line WhatsApp replies must carry',
      priority: 100,
      async execute(_user, _message, response, channel) {
        if (channel !== 'whatsapp') return { modifiedResponse: response };
        return { modifiedResponse: `${response}\n\nReply STOP to opt out.` };
      },
    });
    ```
  </Step>

  <Step title="Register both on the agent">
    Only processors referenced from `LuaAgent` are compiled.

    ```ts src/index.ts highlight={8-9} theme={null}
    import { LuaAgent } from 'lua-cli';
    import piiGuard from './preprocessors/PiiGuard';
    import whatsappFooter from './postprocessors/WhatsAppFooter';

    export default new LuaAgent({
      name: 'support-assistant',
      persona: 'You are the Acme support assistant.',
      preProcessors: [piiGuard],
      postProcessors: [whatsappFooter],
    });
    ```
  </Step>

  <Step title="Test them locally">
    `lua test` runs one processor on your machine with the input you give it; no model is involved.

    ```bash theme={null}
    lua test preprocessor --name pii-guard --input '{"message":"my card is 4111 1111 1111 1111","channel":"web"}' --ci
    ```

    ```text Output theme={null}
    ✅ Selected preprocessor: pii-guard
    🚀 Executing preprocessor...
    Input message: my card is 4111 1111 1111 1111
    Channel: web
    ✅ PreProcessor execution successful!

    Action: BLOCK
    Response: Please don't share card numbers here. Use the secure payment link instead.
    ```

    ```bash theme={null}
    lua test postprocessor --name whatsapp-footer --input '{"message":"when does it ship?","response":"Your order ships tomorrow.","channel":"whatsapp"}' --ci
    ```

    ```text Output theme={null}
    ✅ Selected postprocessor: whatsapp-footer
    🚀 Executing postprocessor...
    Original message: when does it ship?
    Agent response: Your order ships tomorrow.
    Channel: whatsapp
    ✅ PostProcessor execution successful!
    Processed response: Your order ships tomorrow.

    Reply STOP to opt out.
    ```

    To see both in a real conversation before releasing, use `lua chat -e sandbox -m "reach me at jane@example.com"`: sandbox chat uploads your processors as sandbox versions and runs them on the platform, with no push.
  </Step>

  <Step title="Release them">
    `lua push` uploads a version and changes nothing for end users; `lua version create` snapshots the agent; `lua version promote <n>` makes that snapshot live and is also the rollback path ([Release an agent to production](/ship/releasing)).

    ```bash theme={null}
    lua push all --ci --force
    lua version create --ci -m "PII guard and WhatsApp footer"
    lua version promote <n>
    ```

    `lua version create` prints `✓ Created v<n> (staged)`; in a script, `n=$(lua version list --limit 1 --json --ci | jq -r '.[0].version')` then `lua version promote "$n"`. `lua push all` exits 0 even when a primitive fails; check its output for `component(s) failed to push`.

    <Warning>
      From the promote on, both processors run on every message and reply in production, on every channel. A preprocessor that throws is skipped on text channels but blocks the turn on a voice call.
    </Warning>
  </Step>

  <Step title="Verify">
    Send a message the guard should block, then read the log.

    ```bash theme={null}
    lua chat -e production -m "my card is 4111 1111 1111 1111"
    lua logs --type preprocessor --limit 5
    ```

    The reply is the guard's `response`, word for word, and the log shows the blocked turn. `lua logs --type postprocessor --limit 5` shows the footer being applied to WhatsApp replies.
  </Step>
</Steps>

## Options you may need

### Order several processors

Active processors of each kind run in ascending `priority`, each receiving the previous one's output. Two processors with the same priority run in the order the database returns them, which you cannot set, so give each a distinct number.

### Deploy one processor on its own

`lua deploy preprocessor` and `lua deploy postprocessor` are single-primitive shortcuts: each creates and promotes an agent version scoped to that processor, so it goes live immediately and appears in `lua version list`.

```bash theme={null}
lua push preprocessor --name pii-guard --ci --force
lua deploy preprocessor --name pii-guard --set-version latest --force
```

### Block silently

`response` on a block is what the end user sees, and an empty string is honored differently per client: the legacy stream shows nothing, the widget shows the default text `User not eligible for agent response`, and a non-streaming chat call returns `preprocessor_blocked` with empty text. Set `response` whenever the end user should know why.

## If it isn't working

To turn a processor off in production without a rollback, run `lua preprocessors deactivate --preprocessor-name pii-guard --ci` (or `lua postprocessors deactivate --postprocessor-name whatsapp-footer --ci`); `activate` turns it back on.

<AccordionGroup>
  <Accordion title="No preprocessors found in compiled output.">
    **Cause** `lua test` only sees processors registered on `LuaAgent.preProcessors` or `postProcessors`. **Fix** Import the processor in `src/index.ts`, add it to the right array, and run the test again.
  </Accordion>

  <Accordion title="The widget briefly shows the reply without the footer">
    **Cause** On streaming channels the end user sees the reply as the model produces it; postprocessors run when the stream ends and the client then receives the final text. **Fix** When the unprocessed text must never appear, use the non-streaming [chat endpoint](/reference/rest/chat).
  </Accordion>

  <Accordion title="The message reached the model unchanged">
    **Cause** The preprocessor threw or timed out, so the platform skipped it and continued; or another preprocessor with a lower `priority` blocked or replaced the message first. **Fix** Read `lua logs --type preprocessor --limit 5` for the error, and compare the `priority` values of your preprocessors in code.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="About processors" href="/concepts/processors">Order, failure handling, streaming, and processors versus skills.</Card>
  <Card title="PreProcessor reference" href="/reference/sdk/preprocessor">Config fields and the result shape.</Card>
  <Card title="PostProcessor reference" href="/reference/sdk/postprocessor">Config fields and the response shape.</Card>
  <Card title="Add human handoff" href="/build/add-human-handoff">A blocking preprocessor driven by a flag on the end user.</Card>
</Columns>
