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

```sh
npx zavudev fn triggers add --events message.inbound --senders snd_abc
```

When a `message.inbound` event fires for sender `snd_abc`, the function
runs.

> **Tip:**
> If you only want a chatbot, you usually don't need triggers — `defineAgent`
> already binds the agent to its sender. Use triggers when you want **raw
> event delivery**: custom logic before/after the agent, broadcasts, system
> events, or stand-alone automation without an LLM.

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

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

### Single event, all senders in the project

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

### Multiple events × multiple senders (cartesian)

```sh
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

```sh
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:

```sh
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:

```ts
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](https://www.zavu.dev/docs/guides/receiving-messages/events) —
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:

```sh
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

```sh
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:

```ts
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](https://www.zavu.dev/docs/guides/receiving-messages/webhooks) — 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**

    ```ts
    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**

    ```ts
    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,
      })
    })
    ```
    ```sh
    npx zavudev fn triggers add --events broadcast.status_changed --senders any
    ```
  
  
**Welcome new contacts**

    ```ts
    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

```sh
# 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"
```
