Skip to main content

Overview

LuaTrigger (created with defineTrigger) wakes your agent when an external service posts to a trigger URL. Unlike a webhook, a trigger has no execute function — the agent turn itself does the work. Your only customisation surface is three optional, declarative slots that run server-side before the agent is invoked:
  1. verify — authenticate the request (e.g. an HMAC signature check). Return false and the request is rejected with HTTP 401; the agent never runs.
  2. filter — decide whether this event matters. Return false and the sender gets HTTP 200, but no agent invocation happens.
  3. transform — shape what the agent receives. Return a message string (or a full invocation input) instead of the raw payload.
After lua push, every event that passes verify and filter starts an agent turn with the transformed message. There is no handler code to maintain — the agent’s persona, skills, and tools take it from there.
A trigger needs at least one of verify, filter, or transform — a trigger with none of the three is just a paste-anywhere URL trigger, which you can create without any code via lua triggers create.

Triggers vs Webhooks vs Jobs

Reach for LuaWebhook when the caller needs a specific response body or you want to run code without involving the agent. Reach for LuaTrigger when the right reaction to an event is an agent turn — triage this PR, answer this ticket, follow up on this order. See LuaWebhook and LuaJob.

Use Cases

Git Events

“A PR was assigned to me — triage it” with signature verification

Payment Events

Wake the agent on a successful Stripe payment, ignore the rest

Alerting

Point a monitoring tool at the trigger URL; the agent investigates

Automation Glue

Any service that can POST JSON can start an agent turn

Defining a Trigger

Three authoring shapes are recognised — pick whichever fits your codebase:

Configuration Parameters

Required Fields

string
required
Unique trigger name, used as the server-side identifier.Format: URL-safe — a lowercase letter followed by lowercase letters, digits, and hyphens (e.g. 'order-created', 'github-pr-assigned'). Other names compile with a warning.
string
required
Short description shown in trigger listings. It is a note for you — it is not sent to the agent.

Slots (at least one required)

function
Authentication gate. Return false (or a promise of false) to reject the request with HTTP 401 and skip the agent entirely. Put HMAC signature checks here.Signature: (ctx: TriggerContext) => boolean | Promise<boolean>
function
Relevance gate, run after verify. Return false to acknowledge the event with HTTP 200 but skip the agent — the way to intentionally ignore events you don’t care about, without making the sender think delivery failed.Signature: (ctx: TriggerContext) => boolean | Promise<boolean>
function
Shapes the agent input, run after filter. Return either:
  • a string — becomes the agent’s message for the turn, or
  • a full invocation input object ({ messages | prompt, userId?, threadId?, systemPrompt?, ... }) — you own the whole turn.
Returning null/undefined is an error (the delivery fails with HTTP 500) — use filter to skip events, not transform.Signature: (ctx: TriggerContext) => string | AgentInvocationInput | Promise<string | AgentInvocationInput>Omit transform to use the default payload-forwarding format described in Default Agent Message.

Optional Fields

'webhook'
default:"'webhook'"
Event source. Only 'webhook' (an HTTP POST to the trigger URL) is currently supported.
ZodType
Optional Zod schema describing the event body. To type ctx.body in your slots, also pass the payload type as the generic: defineTrigger<MyEvent>({ ... }).

TriggerContext

Every slot receives the same context object:
Compute HMAC signatures over ctx.rawBody, never over JSON.stringify(ctx.body). Providers like GitHub (x-hub-signature-256: sha256=…), Stripe, and Slack sign the exact bytes they send. Re-stringifying the parsed body does not reproduce those bytes (key order, whitespace, and unicode escapes all differ), so a signature computed from ctx.body will fail even for genuine requests. rawBody exists precisely for this.
Header keys arrive lowercased — read ctx.headers['x-hub-signature-256'], not ctx.headers['X-Hub-Signature-256'].

Slot Semantics

Slots run server-side, in order, on every delivery. Their outcome decides the HTTP response the sender sees: A few facts worth knowing:
  • Slots run in a sandbox with a 15-second budget. Keep them fast and computational — signature checks, field comparisons, string building. Don’t make network calls from slots; if the agent needs external data, let the agent fetch it with its tools during the turn.
  • verify failures return 401, not 500. Webhook providers treat a 401 as a configuration error and won’t retry-storm you the way they would on a 5xx.
  • filter failures return 200. The sender sees a successful delivery, so it won’t retry an event you deliberately ignored.
  • Node’s built-in crypto module is available in slots (for createHmac, timingSafeEqual), and env('KEY') reads your agent’s environment variables — the sanctioned way to get secrets into a verify check.
  • The agent turn is fire-and-forget. Once the slots pass, the sender gets its 200 immediately; the agent runs in the background and the outcome lands in the execution log.

Default Agent Message

When you omit transform, the agent receives the event in a standard format. With an instruction set (via lua triggers create --instruction "..."):
Without an instruction, the JSON payload directly follows the prefix — there is no Payload: label:
  • The [Trigger: <name>] prefix is always present, so the agent (and any follow-up turns) can tell what started the conversation.
  • The instruction is the place to tell the agent what to do with the event.
  • The payload is capped at 50,000 characters; anything longer is truncated.
Providing a transform overrides all of this:
  • Return a string → the agent’s message becomes [Trigger: <name>] <your string>. The trigger’s instruction is not applied — your transform owns the message. This is the way to forward hand-picked fields from large payloads instead of hitting the 50k cap.
  • Return an invocation input object → you own the entire turn: message content, userId (to run as a specific user with conversation history), threadId, systemPrompt, and so on. The channel is always recorded as trigger.

Trigger URLs and Token Security

Every trigger gets a URL of the form:
  • The token is the secret — anyone who has the full URL can fire the trigger (subject to your verify slot). Treat the URL like a credential.
  • Unknown agent, unknown token, or a mismatched pair all return the same 404 — the URL shape leaks nothing about which part was wrong.
  • If a URL leaks, rotate it: lua triggers rotate-token --trigger <name>. The old URL stops working immediately and the CLI prints the new one.
A trigger without a verify slot accepts any request that has the URL. That is fine for low-stakes automation glue, but for anything that causes real side effects, add a verify slot with a proper signature check — don’t rely on URL secrecy alone.

Complete Example: GitHub Pull Request Trigger

A production-shaped trigger: HMAC signature verification over the raw bytes, a filter that only lets through PRs assigned to a configured user, and a compact transform (a raw GitHub PR payload is enormous and mostly noise for the agent).
Configure the trigger URL and the same secret in your repository’s webhook settings (Repository → Settings → Webhooks, content type application/json), and set GITHUB_WEBHOOK_SECRET and GITHUB_USERNAME with lua env.

Minimal Example: Filter Only

The smallest useful trigger — no verification, no transform, just a relevance gate. The agent receives the default [Trigger: …] Payload: … message.
This trigger accepts any POST that has the URL — there is no verify slot. Acceptable for internal tooling behind URL secrecy; not acceptable for anything a third party could abuse. Rotate the token immediately if the URL leaks.

Using with LuaAgent

Register triggers on your agent configuration:
On lua push, each trigger is compiled, versioned, and deployed like any other primitive, and your lua.skill.yaml gains a triggers: section tracking the deployed name, ID, and version:

Observability

Every delivery — invoked, filtered, rejected, or failed — is recorded as an execution. Inspect them with the CLI:
See the Triggers Command for the full management workflow — listing, creating URL triggers, activating, rotating tokens, and deleting.

Triggers Command

Manage triggers from the CLI — list, logs, rotate-token

LuaWebhook

Full request/response control with your own execute function

LuaJob

Scheduled and queued background work

Environment Variables

env(‘KEY’) — secrets for your verify slot

See Also