> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zapier.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Using Triggers

> Triggers let your code react to real-time events across 9,000+ Zapier-connected apps (a new GitHub issue, an incoming Slack message, a row added to Google Sheets) without managing subscription state or worrying about the reliability of webhook consumers.

export const appCount = "9,000+";

<Info>
  Import from `"@zapier/zapier-sdk/experimental"` to use these
  methods. For the CLI, run commands via the `zapier-sdk-experimental` binary,
  pass `--experimental` to `zapier-sdk`, or set `ZAPIER_EXPERIMENTAL=true` in
  the environment. Methods and behavior may change between versions.
</Info>

The model is simple: you create a **trigger inbox** that subscribes to a specific `(app, action, connection, inputs)` combination. Zapier handles the subscription and buffers incoming events. Your code leases messages from the inbox and processes them at its own pace.

> **CLI and SDK, complementary by design**
>
> <p>
>   The CLI is the fastest way to explore: find the right action key, inspect
>   what inputs a subscription needs, create a trigger inbox, and interactively test
>   it before writing any code. For scripting and one-off runs, the CLI alone may be
>   enough.
> </p>
>
> <p>
>   When you need a production-grade consumer (a typed handler, a long-running
>   process with graceful shutdown, an AI agent reacting to events in code),
>   reach for the TypeScript SDK. Everything you explored with the CLI transfers
>   directly.
> </p>

***

## Prerequisites

Install and authenticate the CLI as shown in the [quickstart](/sdk/quickstart).

**Experimental entry point** — import from `@zapier/zapier-sdk/experimental` to access Triggers. The factory is the same `createZapierSdk` you already know:

```typescript theme={null}
import { createZapierSdk } from "@zapier/zapier-sdk/experimental";

const zapier = createZapierSdk();
```

For the CLI, opt into the experimental surface in any one of three equivalent ways:

```bash theme={null}
# 1. Dedicated bin (the shim pushes --experimental onto argv for you)
npx zapier-sdk-experimental --help

# 2. argv flag on the stable bin
npx zapier-sdk --experimental --help

# 3. Env var (handy for shells and CI)
ZAPIER_EXPERIMENTAL=true npx zapier-sdk --help
```

***

## Key Concepts

**Trigger** — emits events when something happens in a connected app. Every integration defines its own triggers with their own action keys. Use `listTriggers` to discover what a specific app supports.

**Trigger Inbox** — a server-side subscription you register for a specific `(app, action, connection, inputs)` combination. Zapier connects to the app and buffers incoming events for you.

Inboxes move through a small lifecycle:

1. `initializing` (Zapier is setting up the subscription)
2. `active` (collecting events)
3. `paused` (collection stopped, buffered messages preserved)
4. `deleting` (scheduled for removal)
5. `initialization_failure` (setup failed; check `paused_reason`)

**Message** — an event buffered in the inbox. Each has an `id`, `created_at`, `status`, a `payload` with the raw event data from the app, and `message_attributes` with `lease_count` (how many times it's been leased), `error_message` (last processing error), and `possible_duplicate_data` (flag if deduplication wasn't possible).

**Lease and ack** — messages are leased (hidden from other consumers) while you process them, then acked (permanently removed) when processing succeeds. If your handler throws, the message returns to the available pool when the lease expires. `drainTriggerInbox` and `watchTriggerInbox` manage leasing, acking, and retries for you automatically.

***

## Walkthrough: Subscribe to Slack messages

### Step 1: Discover triggers for an app

`listTriggers` returns all triggers available for an app. The `key` field is what you pass as `action` when creating an inbox.

#### TypeScript SDK

```typescript theme={null}

const { data: triggers } = await zapier.listTriggers({ app: "slack" });

for (const trigger of triggers) {
  console.log(`${trigger.key}: ${trigger.title}`);
}
```

#### CLI

```bash theme={null}
npx zapier-sdk-experimental list-triggers slack
```

```json Response theme={null}
{
  "data": [
    {
      "id": "core:2830101",
      "key": "channel_message",
      "title": "New Message Posted to Channel",
      "description": "Triggers when a new message is posted to a specific #channel you choose.",
      "is_important": false,
      "is_hidden": false,
      "app_key": "SlackCLIAPI",
      "app_version": "1.27.1",
      "action_type": "read",
      "type": "action"
    },
    {
      "id": "core:2830105",
      "key": "mention",
      "title": "New Mention",
      "description": "Triggers when a username or highlight word is mentioned in a public #channel.",
      "is_important": false,
      "is_hidden": false,
      "app_key": "SlackCLIAPI",
      "app_version": "1.27.1",
      "action_type": "read",
      "type": "action"
    },
    ...
  ],
  "errors": []
}
```

### Step 2: Inspect a trigger's required inputs

Use `listTriggerInputFields` to see what inputs the subscription requires. These define the scope of events that get buffered — for example, which Slack channel to watch.

#### TypeScript SDK

```typescript theme={null}

const { data: fields } = await zapier.listTriggerInputFields({
  app: "slack",
  action: "channel_message",
  connection: connection.id,
});

for (const field of fields) {
  if (field.type === "input_field") {
    console.log(
      `${field.key} (${field.is_required ? "required" : "optional"}): ${field.title}`
    );
  }
}
```

#### CLI

```bash theme={null}
npx zapier-sdk-experimental list-trigger-input-fields slack channel_message \
  --connection 12345678
```

```json Response theme={null}
{
  "data": [
    {
      "type": "input_field",
      "key": "channel",
      "default_value": "",
      "depends_on": [],
      "description": "Only channels you are a member of will appear in this list. ...",
      "invalidates_input_fields": false,
      "is_required": true,
      "placeholder": "",
      "title": "Channel",
      "value_type": "STRING",
      "format": "SELECT"
    },
    {
      "type": "input_field",
      "key": "listen_for_bots",
      "default_value": "no",
      "depends_on": [],
      "description": "If `no`, only messages sent by users will trigger. ...",
      "invalidates_input_fields": false,
      "is_required": false,
      "placeholder": "no",
      "title": "Trigger for Bot Messages?",
      "value_type": "BOOLEAN",
      "format": "SELECT"
    },
    {
      "type": "input_field",
      "key": "raw",
      "default_value": "no",
      "depends_on": [],
      "description": "Select `Yes` for a faster, more efficient data fetch. ...",
      "invalidates_input_fields": false,
      "is_required": false,
      "placeholder": "no",
      "title": "Optimized data",
      "value_type": "BOOLEAN",
      "format": "SELECT"
    }
  ],
  "errors": []
}
```

The `key` values are what you'll pass as `inputs` when creating the inbox.

### Step 3: Ensure an inbox

`ensureTriggerInbox` is generally recommended for production use: it's idempotent on `name`, so restarting your process or re-deploying doesn't create duplicate subscriptions. Use `createTriggerInbox` only when you want a throwaway inbox with an auto-generated name.

#### TypeScript SDK

```typescript theme={null}

const { data: inbox } = await zapier.ensureTriggerInbox({
  name: "my-slack-inbox",
  app: "slack",
  action: "channel_message",
  connection: connection.id,
  inputs: { channel: "C0123ABC456" },
});

console.log(`Inbox ready: ${inbox.id} (${inbox.status})`);
```

#### CLI

```bash theme={null}
npx zapier-sdk-experimental ensure-trigger-inbox my-slack-inbox slack channel_message \
  --connection 12345678 \
  --inputs '{"channel": "C0123ABC456"}'
```

```json Response theme={null}
{
  "data": {
    "id": "01960a3e-4d5f-7b8e-9c0a-1b2c3d4e5f6a",
    "created_at": "2026-05-01T12:34:56.000000Z",
    "name": "my-slack-inbox",
    "status": "active",
    "paused_reason": null,
    "notification_url": null,
    "subscription": {
      "connection_id": "12345678",
      "app_key": "SlackCLIAPI@1.27.1",
      "action_key": "channel_message",
      "inputs": { "channel": "C0123ABC456" }
    }
  },
  "errors": []
}
```

The inbox is now active: Zapier is watching for new Slack messages and buffering them.

### Step 4: Drain messages once

`drainTriggerInbox` leases all currently-available messages, calls your `onMessage` handler for each, and resolves when the inbox is empty (or `maxMessages` is reached). Right for one-shot processing: a cron job, a script, or a webhook handler.

#### TypeScript SDK

```typescript theme={null}

await zapier.drainTriggerInbox({
  inbox: "my-slack-inbox",
  onMessage: async (message) => {
    const { text, user, channel } = message.payload as {
      text: string;
      user: string;
      channel: string;
    };
    console.log(`New Slack message in ${channel} from ${user}: ${text}`);
    // Returning resolves the handler: the message is acked and removed from the inbox.
    // Throwing rejects it: with releaseOnError true, it returns to the pool when the drain finishes.
  },
  releaseOnError: true,
});
```

The handler receives the full `message` object including `message.payload` (the raw event data). Return from the handler to ack the message; throw to trigger release or retry behavior.

**Key options:**

* `--max-messages` / `maxMessages` caps how many to drain.
* `--concurrency` / `concurrency` runs multiple handlers in parallel.
* `--release-on-error` / `releaseOnError` releases failed messages when the drain finishes, instead of waiting for the full lease timeout.

Refer to the [Triggers API reference](/sdk/reference#draintriggerinbox) for the full list of options.

#### CLI

```bash theme={null}
npx zapier-sdk-experimental drain-trigger-inbox my-slack-inbox --json
```

For custom per-message processing, pipe the message JSON from stdin to your own script or command:

```bash theme={null}
npx zapier-sdk-experimental drain-trigger-inbox my-slack-inbox \
  --exec node -- ./handle-message.js
```

Use `--exec-shell` instead when you need shell features like pipes, redirects, or env expansion (e.g. `--exec-shell "./handler | jq .payload"`).

Refer to the [Triggers CLI reference](/sdk/cli-reference#drain-trigger-inbox) for the full list of options.

### Step 5: Watch continuously

`watchTriggerInbox` runs indefinitely, consuming messages as they arrive. It drains all currently-available messages, then holds open a Server-Sent Events (SSE) connection and re-drains the moment Zapier notifies it of new arrivals, so wake-ups are near-real-time. A periodic safety drain runs as a backstop in case a notification is missed. Use it for long-running services (e.g. a server-side consumer/worker, or a CLI tool that runs continuously).

#### TypeScript SDK

```typescript theme={null}

const controller = new AbortController();

// Abort cleanly on shutdown signals.
process.on("SIGTERM", () => controller.abort());
process.on("SIGINT", () => controller.abort());

await zapier.watchTriggerInbox({
  inbox: "my-slack-inbox",
  onMessage: async (message) => {
    const { text, user, channel } = message.payload as {
      text: string;
      user: string;
      channel: string;
    };
    console.log(`New Slack message in ${channel} from ${user}: ${text}`);
  },
  releaseOnError: true,
  signal: controller.signal,
});
```

The `AbortController` gives you clean shutdown: aborting closes the SSE connection, cancels in-flight HTTP requests, releases unprocessed messages back to the inbox, and resolves the call rather than rejecting it. Hook it to `SIGTERM` and `SIGINT` so your process shuts down gracefully.

**Run under a process supervisor** — for production, keep the watcher alive across crashes and reboots with a supervisor like [pm2](https://pm2.keymetrics.io/), [systemd](https://systemd.io/) (Linux), [launchd](https://www.launchd.info/) (macOS), or [brew services](https://github.com/Homebrew/homebrew-services) (macOS, dev). Configure the supervisor to send `SIGTERM` on stop so the shutdown handling above runs cleanly.

#### CLI

```bash theme={null}
npx zapier-sdk-experimental watch-trigger-inbox my-slack-inbox \
  --exec node -- ./handle-message.js
```

`--max-drain-interval-seconds` sets the safety-drain interval, the longest the watcher will go without draining if no SSE notification arrives (default: 300s). If real-time wake-ups pause, the watcher logs a warning to stderr and falls back to that safety drain (run with `--debug` for transient reconnect notices); stdout, including `--json` NDJSON, stays clean for piping. `--exec` and `--exec-shell` work the same as in `drain-trigger-inbox`.

Refer to the [Triggers CLI reference](/sdk/cli-reference#watch-trigger-inbox) for the full list of options.

#### How the watcher wakes up

After the initial drain, `watchTriggerInbox` keeps a single SSE connection open to Zapier and re-drains the inbox whenever a notification arrives, so new messages are picked up in near-real-time rather than on a fixed interval. Three things can trigger a drain:

* **An SSE notification** — the normal path; fires within moments of a new message landing in the inbox.
* **The safety drain** — a periodic backstop (every `maxDrainIntervalSeconds`, default 300s) that runs regardless of SSE state, so the inbox continues to be processed even if a notification is missed or the connection drops undetected.
* **Connection (re)open** — the watcher drains once whenever the SSE connection is established, to catch anything that arrived while it was offline.

If the connection drops, the watcher reconnects automatically with backoff while the safety drain covers the gap. Connection health is reported on stderr: a warning when real-time wake-ups pause and the watcher falls back to the safety drain, plus transient reconnect notices when debug logging is enabled (`--debug` on the CLI, or `debug: true` in the SDK options). This output is informational and does not require action.

***

## Under the hood: lease, ack, release

`drainTriggerInbox` and `watchTriggerInbox` compose three lower-level operations that are worth understanding when debugging unexpected behavior:

**Lease** — `leaseTriggerInboxMessages` reserves a batch of messages for a configurable lease window (see the [Triggers API reference](/sdk/reference) for defaults and limits). While leased, messages are hidden from other consumers. `message_attributes.lease_count` increments each time a message is leased.

Once `lease_count` reaches **5**, the message transitions to `quarantined`. Quarantined messages are no longer returned via `lease`, `drain`, or `watch`, but they remain visible in `listTriggerInboxMessages` output. Quarantine is a fail-safe for the experimental period: if a bug causes processing to fail at scale, we can identify the cause, fix it, and clear leases server-side. There is no end-user mechanism to recover a quarantined message today, so design your handlers to drop known-bad messages before they hit the threshold (see *Ack-to-drop poison messages*  below).

**Ack** — `ackTriggerInboxMessages` permanently removes messages from the inbox. The drain/watch API acks automatically when your handler returns without throwing.

**Release** — `releaseTriggerInboxMessages` returns messages to the available pool before the lease expires. Use it when you want to give up on a batch early without waiting for timeout.

**Ack-to-drop poison messages** — if a message keeps failing for reasons your handler can detect (malformed payload, missing required field, repeated upstream error), check `message.message_attributes.lease_count` and explicitly ack it to remove it from the inbox before it quarantines. Pairs well with `continueOnError: true`, so one bad message doesn't reject the whole drain.

**Control-flow signals from `onMessage`** give fine-grained control without rejecting the entire drain:

* Throw `ZapierReleaseTriggerMessageSignal` to release just this one message and continue draining.
* Throw `ZapierAbortDrainSignal` to finish the current batch, then resolve the drain cleanly.

**`releaseOnError: true`** — by default, when `onMessage` throws, the message stays leased until the timeout. Pass `releaseOnError: true` to release it when the drain finishes instead, so it becomes available for re-processing without waiting out the full lease.

**`continueOnError: true`** — by default, the first handler error rejects the entire drain (fail-fast). Pass `continueOnError: true` to keep draining: handler errors are routed to an optional `onError(error, message)` observer, and the message is then released-or-left per `releaseOnError`. SDK-level errors (lease/ack/release HTTP failures) still reject regardless. On the CLI, the equivalent flag is `--continue-on-error`.

```typescript theme={null}
import {
  createZapierSdk,
  ZapierReleaseTriggerMessageSignal,
  ZapierAbortDrainSignal,
} from "@zapier/zapier-sdk/experimental";
```

For the full primitive API (`leaseTriggerInboxMessages`, `ackTriggerInboxMessages`, `releaseTriggerInboxMessages`), see the [Triggers API reference](/sdk/reference).

***

## Inbox management

**List inboxes** — find inboxes by status or name:

#### TypeScript SDK

```typescript theme={null}

const { data: inboxes } = await zapier.listTriggerInboxes({ status: "active" });

for (const inbox of inboxes) {
  console.log(
    `${inbox.name}: ${inbox.status} (${inbox.subscription.app_key}/${inbox.subscription.action_key})`
  );
}
```

#### CLI

```bash theme={null}
npx zapier-sdk-experimental list-trigger-inboxes
npx zapier-sdk-experimental list-trigger-inboxes --status active
npx zapier-sdk-experimental list-trigger-inboxes --name my-slack-inbox
```

***

**Pause and resume** — pausing stops event collection without discarding buffered messages. Resuming restarts it.

#### TypeScript SDK

```typescript theme={null}

// Pause stops event collection; buffered messages are preserved.
const { data: paused } = await zapier.pauseTriggerInbox({ inbox: "my-slack-inbox" });
console.log(paused.status); // "paused"

// Resume restarts event collection.
const { data: resumed } = await zapier.resumeTriggerInbox({ inbox: "my-slack-inbox" });
console.log(resumed.status); // "active"
```

#### CLI

```bash theme={null}
npx zapier-sdk-experimental pause-trigger-inbox my-slack-inbox
npx zapier-sdk-experimental resume-trigger-inbox my-slack-inbox
```

***

**Delete** — marks the inbox for deletion and cancels the server-side subscription:

```typescript theme={null}
await zapier.deleteTriggerInbox({ inbox: "my-slack-inbox" });
```

```bash theme={null}
npx zapier-sdk-experimental delete-trigger-inbox my-slack-inbox
```

***

**Update** — currently only `notificationUrl` can be updated on an existing inbox:

```typescript theme={null}
await zapier.updateTriggerInbox({
  inbox: "my-slack-inbox",
  notificationUrl: "https://my-server.example/webhook",
});
```

***

## Webhook delivery: `notificationUrl`

`watchTriggerInbox` keeps a persistent SSE connection open and reacts to Zapier's notifications, which suits a long-running process. If you'd rather not hold a connection open (for example, a serverless or stateless consumer) and you operate a server with a public endpoint, pass `notificationUrl` when creating the inbox instead. Zapier POSTs to that URL whenever new messages arrive, so your endpoint can call `drainTriggerInbox` on-demand.

#### TypeScript SDK

```typescript theme={null}

const { data: inbox } = await zapier.ensureTriggerInbox({
  name: "my-slack-inbox-push",
  app: "slack",
  action: "channel_message",
  connection: connection.id,
  inputs: { channel: "C0123ABC456" },
  notificationUrl: "https://my-server.example/trigger-webhook",
});

console.log(`Zapier will POST to ${inbox.notification_url} when new messages arrive`);
```

#### CLI

```bash theme={null}
npx zapier-sdk-experimental ensure-trigger-inbox my-slack-inbox-push slack channel_message \
  --connection 12345678 \
  --inputs '{"channel": "C0123ABC456"}' \
  --notification-url https://my-server.example/trigger-webhook
```

Your webhook endpoint receives the POST and immediately calls `drainTriggerInbox` to process the buffered messages. This is the right pattern for consumers that can't hold a persistent connection open, such as serverless functions.

***

## Next Steps

* [Triggers API reference](/sdk/reference) — full parameter docs for every trigger method
* [CLI reference](/sdk/cli-reference) — all `zapier-sdk-experimental` commands
* [Deploy with client credentials](/sdk/deploy) — run your trigger consumer in production without browser-based authentication
* [Give feedback](https://npsup.zapier.app/contact-us?product=Zapier%20SDK) — your input shapes what we build next
