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:
verify— authenticate the request (e.g. an HMAC signature check). Returnfalseand the request is rejected with HTTP 401; the agent never runs.filter— decide whether this event matters. Returnfalseand the sender gets HTTP 200, but no agent invocation happens.transform— shape what the agent receives. Return a message string (or a full invocation input) instead of the raw payload.
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.
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: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.
verifyfailures 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.filterfailures return 200. The sender sees a successful delivery, so it won’t retry an event you deliberately ignored.- Node’s built-in
cryptomodule is available in slots (forcreateHmac,timingSafeEqual), andenv('KEY')reads your agent’s environment variables — the sanctioned way to get secrets into averifycheck. - 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 omittransform, the agent receives the event in a standard format. With an instruction set (via lua triggers create --instruction "..."):
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.
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 astrigger.
userId takes precedence over the trigger’s own bound user — see Who the trigger turn runs as.
Who the trigger turn runs as
Every trigger has a bound user, and the agent turn it fires runs as that person:- A trigger you create yourself —
lua pushof aLuaTrigger, orlua triggers create— binds you, the developer who created it. - A trigger that arrives with an installed template binds the installer.
Inbox.push from a tool running in the turn reaches the bound user, User reads and writes act on them, and the conversation lands in their history — instead of a context-less system run.
Identity precedence, highest first:
- A transform-supplied
userId. Atransformthat returns a full invocation input withuserIdowns the turn’s identity — the SDK author’s explicit choice always wins. - The trigger’s bound user. The default when the transform sets no
userId(or there is no transform). - System. With no transform
userIdand no binding, the turn runs with a system identity and no ambient user.
- It never comes from the request. The bound user is set server-side when the trigger is created or installed — nothing in the webhook payload, headers, or URL can choose or change who the turn runs as.
- It doesn’t survive duplication. Duplicating an agent gives the copy’s triggers fresh URLs and no bound user — a copy must never fire as the original’s owner. Its turns run with system identity until the binding is re-established (re-create the trigger, or re-install the template on the copy).
- It doesn’t outlive the user. If the bound user’s account is offboarded, the binding is cleared and the trigger falls back to system identity rather than acting for someone who left.
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
verifyslot). 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.
Agents know their own URL
When an agent is installed from a template, the platform writes each webhook trigger’s per-install URL into the agent’s environment asLUA_TRIGGER_URL__<TRIGGER_KEY> — the trigger key upper-snaked, so github-pr-assigned becomes LUA_TRIGGER_URL__GITHUB_PR_ASSIGNED. The variable is readable with env() from tools and from trigger slots, is rewritten if the trigger’s token is rotated, and is removed when the template is uninstalled.
This is what makes zero-input webhook setup possible: instead of the installer copying a URL into a provider’s settings screen, a setup tool in the template reads its own URL and registers it with the provider itself.
LUA_TRIGGER_URL__ prefix is reserved: a template’s env contract cannot declare keys under it (publishing rejects them), and a hand-set variable of the same name is overwritten with the platform’s URL at install. The full pattern — pairing the injected URL with a post-install setup turn — is in Publishing Templates.
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).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.
Using with LuaAgent
Register triggers on your agent configuration: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.
Related APIs
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
- LuaAgent - Registering triggers on your agent
- LuaWebhook - When you need to own the HTTP response
- Triggers Command - CLI management and the paste-anywhere URL workflow

