Skip to content

Triggers

Bind your function to Zavu events — message.inbound, broadcast.status_changed, and more.

View .md

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.

terminal
npx 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

EventWhen it firesTypical use
message.queuedOutbound message accepted for sendingTrack outbound throughput
message.sentProvider accepted the outboundUpdate CRM with provider IDs
message.deliveredRecipient device received itConversion tracking
message.readRecipient opened it (WhatsApp only)Engagement metrics
message.failedOutbound permanently failedRetry queues, fallback channel
message.inboundCustomer messaged usCustom bots, escalation logic
message.unsupportedInbound message type we don't handleLog + reply with a fallback
broadcast.status_changedBroadcast state movedPipeline orchestration
conversation.newFirst message ever from a contactWelcome flows, onboarding
template.status_changedWhatsApp template approved/rejectedCI deploys, alerts
invitation.status_changedPartner invitation movedOnboarding follow-up
cronOn a schedule you defineDaily digests, syncs, cleanups

Run npx zavudev fn triggers events for the always-current list.

Adding triggers

Single event, single sender

terminal
npx zavudev fn triggers add \ --events message.inbound \ --senders jn76vnxet8g5nq661by3v06y1581bmmn

Single event, all senders in the project

terminal
npx zavudev fn triggers add --events message.inbound --senders any

Multiple events × multiple senders (cartesian)

terminal
npx 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

terminal
npx 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:

terminal
npx 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:

TypeScript
import { 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:

terminal
npx 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

terminal
npx 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.inbound on the same sender

…then two things happen for every inbound message:

  1. The agent processes the message and replies via tools.
  2. Your defineFunction default 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:

TypeScript
export 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:

TraitNative triggersSender webhooks
Setupnpx zavudev fn triggers addConfigure URL + signing secret per sender
AuthZavu-internal signed invocationHMAC SHA-256 over the body
Latency~50ms (direct invoke)Internet round trip
RetriesManaged retries (2x by default)You implement
CostOne function invocationOne function invocation + one outbound HTTP request

Common patterns

Escalate to human on certain keywords
TypeScript
export 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
TypeScript
export 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, }) })
terminal
npx zavudev fn triggers add --events broadcast.status_changed --senders any
Welcome new contacts
TypeScript
export 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"