Triggers
Bind your function to Zavu events — message.inbound, broadcast.status_changed, and more.
Triggers
A trigger says "fire this function when this event happens on this sender." Triggers are how you build event-driven automation without an AI agent — just raw events, your code reacts.
terminalnpx zavudev fn triggers add --events message.inbound --senders snd_abc
When a message.inbound event fires for sender snd_abc, the function
runs.
Event types
| Event | When it fires | Typical use |
|---|---|---|
message.queued | Outbound message accepted for sending | Track outbound throughput |
message.sent | Provider accepted the outbound | Update CRM with provider IDs |
message.delivered | Recipient device received it | Conversion tracking |
message.read | Recipient opened it (WhatsApp only) | Engagement metrics |
message.failed | Outbound permanently failed | Retry queues, fallback channel |
message.inbound | Customer messaged us | Custom bots, escalation logic |
message.unsupported | Inbound message type we don't handle | Log + reply with a fallback |
broadcast.status_changed | Broadcast state moved | Pipeline orchestration |
conversation.new | First message ever from a contact | Welcome flows, onboarding |
template.status_changed | WhatsApp template approved/rejected | CI deploys, alerts |
invitation.status_changed | Partner invitation moved | Onboarding follow-up |
cron | On a schedule you define | Daily digests, syncs, cleanups |
Run npx zavudev fn triggers events for the always-current list.
Adding triggers
Single event, single sender
terminalnpx zavudev fn triggers add \ --events message.inbound \ --senders jn76vnxet8g5nq661by3v06y1581bmmn
Single event, all senders in the project
terminalnpx zavudev fn triggers add --events message.inbound --senders any
Multiple events × multiple senders (cartesian)
terminalnpx zavudev fn triggers add \ --events message.delivered,message.read,message.failed \ --senders snd_abc,snd_xyz # → 6 triggers (3 events × 2 senders)
Mix specific + any
terminalnpx zavudev fn triggers add \ --events message.inbound \ --senders snd_abc,any # → 2 triggers: one for snd_abc, one for "any sender"
Duplicates are deduped silently.
Schedules (cron)
The cron event type runs the function on a schedule instead of a messaging
event. Pass a standard 5-field expression (minute, hour, day-of-month, month,
day-of-week), evaluated in UTC, minimum granularity one minute:
terminalnpx zavudev fn triggers add --events cron --cron '*/15 * * * *' # every 15 min npx zavudev fn triggers add --events cron --cron '0 9 * * 1-5' # weekdays 09:00 UTC
A schedule has no sender dimension, and one function can hold several cron triggers with different expressions. Each fire invokes the function with:
JSON{ "type": "cron", "timestamp": 1736850000000, "data": { "cron": "*/15 * * * *" } }
Pausing a cron trigger (fn triggers toggle --off) stops the schedule;
re-enabling restarts it from now — missed ticks are not replayed.
Handling events in code
When a trigger fires, your function's default handler (defineFunction)
receives the event:
TypeScriptimport { defineFunction } from "@zavudev/functions" export default defineFunction(async (event, ctx) => { // event.type is "message.inbound", "broadcast.status_changed", etc. // event.data has the payload (shape depends on event) // event.senderId is set for sender-scoped events // event.projectId, event.timestamp always present switch (event.type) { case "message.inbound": await handleInbound(event.data) break case "broadcast.status_changed": await handleBroadcastStatus(event.data) break } })
The event shape mirrors the webhook payload — same JSON, same fields. The difference: no HTTP receiver needed, no signature verification, no retries to manage. The dispatcher invokes your function directly with the event in the payload.
Pausing without removing
Toggle a trigger off without deleting:
terminalnpx zavudev fn triggers list # id event sender active # tr_abc message.inbound snd_xyz yes npx zavudev fn triggers toggle tr_abc --off # now active: no — function won't fire on this event npx zavudev fn triggers toggle tr_abc # re-enables
Useful for temporary disabling without losing the trigger configuration.
Removing
terminalnpx zavudev fn triggers rm tr_abc
When triggers + defineAgent coexist
If your function has BOTH:
defineAgent({...})running on a sender, AND- An explicit trigger for
message.inboundon the same sender
…then two things happen for every inbound message:
- The agent processes the message and replies via tools.
- Your
defineFunctiondefault handler also runs (the trigger fires it).
This is by design — you might want raw event access (for logging, custom analytics, escalation logic) on top of the agent. Just make sure your raw handler doesn't also send a reply or you'll double-message the customer.
To avoid duplicate processing, gate your raw handler:
TypeScriptexport default defineFunction(async (event, ctx) => { if (event.type !== "message.inbound") return // Don't send a reply — the agent does that. Just observe. await analytics.track("inbound", { contactPhone: event.data.from, text: event.data.text, }) })
Native vs HTTP webhooks
Triggers are the native way to receive events. The old path — webhooks on senders — still works and is the right choice when your event receiver lives outside Zavu (an n8n flow, a Vercel function, an internal server). Inside Zavu Functions, prefer triggers:
| Trait | Native triggers | Sender webhooks |
|---|---|---|
| Setup | npx zavudev fn triggers add | Configure URL + signing secret per sender |
| Auth | Zavu-internal signed invocation | HMAC SHA-256 over the body |
| Latency | ~50ms (direct invoke) | Internet round trip |
| Retries | Managed retries (2x by default) | You implement |
| Cost | One function invocation | One function invocation + one outbound HTTP request |
Common patterns
Escalate to human on certain keywords
TypeScriptexport default defineFunction(async (event) => { if (event.type !== "message.inbound") return const text = (event.data.text ?? "").toLowerCase() if (/agente|human|hablar con alguien|complaint/.test(text)) { await slack.send( `Customer ${event.data.from} wants a human:\n> ${event.data.text}` ) } })
Add a trigger on message.inbound and the agent on the same sender. Both
run; the LLM responds, your code pages the team.
Track broadcast completion
TypeScriptexport default defineFunction(async (event) => { if (event.type !== "broadcast.status_changed") return if (event.data.status !== "completed") return await analytics.track("broadcast_done", { broadcastId: event.data.id, deliveredCount: event.data.deliveredCount, }) })
terminalnpx zavudev fn triggers add --events broadcast.status_changed --senders any
Welcome new contacts
TypeScriptexport default defineFunction(async (event, ctx) => { if (event.type !== "conversation.new") return const zavu = new Zavudev({ apiKey: process.env.ZAVU_API_KEY! }) await zavu.messages.send({ to: event.data.contactPhone, channel: event.data.channel, text: "¡Bienvenido! Soy Bella, en qué te puedo ayudar?", }) })
API equivalence
terminal# List curl https://api.zavu.dev/v1/functions/$FN_ID/triggers \ -H "Authorization: Bearer $KEY" # Create curl -X POST https://api.zavu.dev/v1/functions/$FN_ID/triggers \ -H "Authorization: Bearer $KEY" \ -d '{"eventTypes":["message.inbound"],"senderIds":["snd_abc",null]}' # null in senderIds means "any sender" # Toggle curl -X PATCH https://api.zavu.dev/v1/functions/triggers/$TRIGGER_ID \ -H "Authorization: Bearer $KEY" \ -d '{"active":false}' # Delete curl -X DELETE https://api.zavu.dev/v1/functions/triggers/$TRIGGER_ID \ -H "Authorization: Bearer $KEY"