Skip to main content

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.

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

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.

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.


Prerequisites

Install and authenticate the CLI as shown in the quickstart. Experimental entry point — import from @zapier/zapier-sdk/experimental to access Triggers. The factory is the same createZapierSdk you already know:
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:
# 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


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

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

CLI

npx zapier-sdk-experimental list-triggers slack
Response
{
  "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


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

npx zapier-sdk-experimental list-trigger-input-fields slack channel_message \
  --connection 12345678
Response
{
  "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


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

npx zapier-sdk-experimental ensure-trigger-inbox my-slack-inbox slack channel_message \
  --connection 12345678 \
  --inputs '{"channel": "C0123ABC456"}'
Response
{
  "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


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 for the full list of options.

CLI

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:
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 for the full list of options.

Step 5: Watch continuously

watchTriggerInbox wraps drainTriggerInbox in a poll loop with backoff. It runs indefinitely, draining whenever messages are available and backing off when the inbox is empty. Use it for long-running services (e.g. a server-side consumer/worker, or a CLI tool that runs continuously).

TypeScript SDK


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 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, systemd (Linux), launchd (macOS), or brew services (macOS, dev). Configure the supervisor to send SIGTERM on stop so the shutdown handling above runs cleanly.

CLI

npx zapier-sdk-experimental watch-trigger-inbox my-slack-inbox \
  --exec node -- ./handle-message.js
--max-drain-interval-seconds caps the backoff between empty-inbox polls (default: 60s). --exec and --exec-shell work the same as in drain-trigger-inbox. Refer to the Triggers CLI reference for the full list of options.

Under the hood: lease, ack, release

drainTriggerInbox and watchTriggerInbox compose three lower-level operations that are worth understanding when debugging unexpected behavior: LeaseleaseTriggerInboxMessages reserves a batch of messages for a configurable lease window (see the Triggers API 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). AckackTriggerInboxMessages permanently removes messages from the inbox. The drain/watch API acks automatically when your handler returns without throwing. ReleasereleaseTriggerInboxMessages 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.
import {
  createZapierSdk,
  ZapierReleaseTriggerMessageSignal,
  ZapierAbortDrainSignal,
} from "@zapier/zapier-sdk/experimental";
For the full primitive API (leaseTriggerInboxMessages, ackTriggerInboxMessages, releaseTriggerInboxMessages), see the Triggers API reference.

Inbox management

List inboxes — find inboxes by status or name:

TypeScript SDK


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

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


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

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:
await zapier.deleteTriggerInbox({ inbox: "my-slack-inbox" });
npx zapier-sdk-experimental delete-trigger-inbox my-slack-inbox

Update — currently only notificationUrl can be updated on an existing inbox:
await zapier.updateTriggerInbox({
  inbox: "my-slack-inbox",
  notificationUrl: "https://my-server.example/webhook",
});

Push instead of poll: notificationUrl

By default, watchTriggerInbox polls for messages with exponential backoff. If you’re operating a server with a public endpoint, pass notificationUrl when creating the inbox. Zapier will POST to that URL whenever new messages arrive, so you can call drainTriggerInbox on-demand instead of running a continuous poll loop.

TypeScript SDK


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

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 eliminates polling delay and is the right pattern when minimizing latency matters.

Next Steps