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

# Create a trigger

> Register a paste-anywhere URL, shape its events with defineTrigger, push and release the trigger version, and read its execution log

After this guide, a URL you paste into any system that sends HTTP requests wakes your agent, and code you wrote checks and shapes each event first. A [trigger](/concepts/triggers) never runs handler code of yours and can't control the HTTP response; when you need either, [handle a webhook](/build/handle-a-webhook) instead.

*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)).
* The sender's payload shape and how it authenticates (a shared header or a body signature).

<Steps>
  <Step title="Create the trigger and copy its URL">
    `lua triggers create` registers the trigger and prints `✅ Trigger "order-created" created`, the URL under `🔗 Trigger URL (paste it anywhere):`, and a `curl` line that fires it. The token in the URL is the only credential; treat the URL like an API key.

    ```bash theme={null}
    lua triggers create --name order-created --description "Fires when the shop records an order"
    ```

    Pasted as it is, the URL already works (`Type: URL` in `lua triggers list`): every POST body becomes a message to the agent, prefixed with `[Trigger: order-created]` and any `--instruction`. The turn runs as you, the creator, with your tools and data, unless `transform` returns another `userId`; no end user receives anything unless the model calls a tool that [sends a message](/build/send-proactive-messages).
  </Step>

  <Step title="Shape events in code">
    `defineTrigger` has no `execute`. Up to four slots run on the platform per delivery, in order: `verify` (401 when `false`), `filter` (200 and dropped when `false`), `transform` (the event becomes the message), and `tool` (one tool, no model turn). The slots share a 15-second budget and can read `env()`.

    ```ts src/triggers/order-created.trigger.ts theme={null}
    import { defineTrigger, env } from 'lua-cli';
    import { z } from 'zod';

    const orderEvent = z.object({
      type: z.string(),
      data: z.object({ orderNumber: z.string(), total: z.number(), customerEmail: z.string() }),
    });

    export default defineTrigger<z.infer<typeof orderEvent>>({
      name: 'order-created',
      description: 'Wakes the agent when the shop reports a new order',
      inputSchema: orderEvent,
      verify: (ctx) => ctx.headers['x-shop-token'] === env('SHOP_WEBHOOK_TOKEN'),
      filter: (ctx) => ctx.body.type === 'order.created',
      transform: (ctx) =>
        `Order ${ctx.body.data.orderNumber} for ${ctx.body.data.total} was placed by ` +
        `${ctx.body.data.customerEmail}. Confirm it and mention the 30-day returns policy.`,
    });
    ```

    The type argument types `ctx.body` in every slot. A sender that signs its payload is verified over `ctx.rawBody`, the exact bytes received. Store the token with `lua env production -k SHOP_WEBHOOK_TOKEN -v <token>`.
  </Step>

  <Step title="Register it and compile">
    Add the trigger to `LuaAgent.triggers`; unreferenced triggers aren't compiled. The file below is the quickstart's; if yours differs, add only the highlighted lines to your own `LuaAgent`.

    ```ts src/index.ts highlight={3,9} theme={null}
    import { LuaAgent } from 'lua-cli';
    import weatherSkill from './skills/weather.skill';
    import orderCreated from './triggers/order-created.trigger';

    const agent = new LuaAgent({
      name: 'docs-quickstart',
      persona: 'You are a weather assistant. Answer in one or two sentences.',
      skills: [weatherSkill],
      triggers: [orderCreated],
    });
    ```

    `lua compile` checks the slots.

    ```bash theme={null}
    lua compile --ci
    ```

    ```text Output theme={null}
    …
    ✅ Compiled 4 primitives (1 agent, 1 skill, 1 tool, 1 trigger) in 498ms
    ```

    `lua compile` fails when no slot is set and warns when both `tool` and `transform` are declared: the tool wins.
  </Step>

  <Step title="Push and release the version">
    `lua push trigger` uploads the compiled slots as a version of the trigger with that name and links the `lua.skill.yaml` row to the trigger from Step 1 by name: one trigger, one URL (the server refuses a duplicate name). Without Step 1, the push creates the trigger and `lua triggers list` prints its URL. `lua deploy trigger` then creates and promotes an agent version scoped to this trigger, so the slots are live at once and `lua triggers list` shows `Type: SDK`.

    ```bash theme={null}
    lua push trigger --ci --force --name order-created
    lua deploy trigger --ci --name order-created --set-version latest --force
    ```

    To release it with other changes instead, skip the deploy: `lua push all --ci --force`, then `lua version create --ci -m "Add order-created trigger"`, which prints ``✓ Created v<n> (staged). Run `lua version promote v<n>` to deploy.``, then `lua version promote <n>` (`<n>` or `v<n>`, no confirmation; in a script `n=$(lua version list --limit 1 --json --ci | jq -r '.[0].version')`) ([Release an agent to production](/ship/releasing)).
  </Step>

  <Step title="Paste the URL and fire it">
    There is no `lua test trigger`, so this request is the first test. Give the sender the URL, or send one event yourself with the header `verify` expects.

    ```bash theme={null}
    curl -X POST "<<TRIGGER_URL>>" \
      -H 'Content-Type: application/json' \
      -H 'x-shop-token: <<SHOP_WEBHOOK_TOKEN>>' \
      -d '{"type":"order.created","data":{"orderNumber":"A-1001","total":120,"customerEmail":"user@example.com"}}'
    ```

    The response is `200` with `{ "status": "accepted", "executionId": "…" }` once the slots pass; the turn runs afterwards. A wrong `x-shop-token` answers `401`; a `type` other than `order.created` answers `200` and is dropped.
  </Step>

  <Step title="Verify">
    Every delivery is a row in the trigger's log for 90 days, newest first, with status, duration, payload, reply, and tools used.

    ```bash theme={null}
    lua triggers logs --trigger order-created --limit 5 --ci
    ```

    Your event shows as `COMPLETED` with the reply text once the turn finishes, or `ACCEPTED (in flight)` while it runs; a bad token shows as `REJECTED (verify failed → 401)` and a filtered event as `SKIPPED (filtered out)`. `--json` returns the rows as data.
  </Step>
</Steps>

## Options you may need

### Run one tool instead of a model turn

Replace `transform` with `tool: { name: 'lookup_order', input: (ctx) => ({ orderId: ctx.body.data.orderNumber }) }`: `name` is a string literal naming a tool in one of the agent's skills, and `input` maps the event to its arguments. No message is sent; the result goes on the log row. To start a [workflow](/concepts/workflows) run instead, return `{ startWorkflow }` from `transform` ([LuaTrigger reference](/reference/sdk/luatrigger)).

### Rotate a leaked token

`lua triggers rotate-token --trigger order-created` prints a replacement URL and the old one stops working at once; `lua triggers deactivate --trigger order-created` pauses deliveries without changing the URL.

## If it isn't working

<AccordionGroup>
  <Accordion title="Every delivery is REJECTED (verify failed → 401)">
    The header the slot compares isn't what the sender sends (keys are lowercase in `ctx.headers`), or `SHOP_WEBHOOK_TOKEN` isn't set in production. Set it with `lua env production -k SHOP_WEBHOOK_TOKEN -v <token>`; slots read it on the next delivery.
  </Accordion>

  <Accordion title="A delivery is FAILED">
    `transform` returned nothing, a slot threw or ran over its budget (the sender got 500), or the turn or tool failed after the 200. Lua never redelivers, so the sender must send again; use `filter`, not an empty `transform`, to skip an event.
  </Accordion>

  <Accordion title="The sender gets 404 and nothing is logged">
    The URL is wrong or its token was rotated. `lua triggers list` prints the current URL; a request that matches no trigger is not recorded.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="LuaTrigger reference" href="/reference/sdk/luatrigger">The context object, every slot, statuses, and startWorkflow fields.</Card>
  <Card title="lua triggers reference" href="/reference/cli/triggers">create, list, logs, activate, rotate-token, delete.</Card>
  <Card title="Handle a webhook" href="/build/handle-a-webhook">When you need your own handler and response.</Card>
  <Card title="Integration events" href="/integrations/events">Route events from a connected SaaS to a trigger.</Card>
</Columns>
