@trigger.dev/sdk 4.7.4 → 4.8.0-webhooks.20261015065339
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/v3/ai.d.ts +560 -8
- package/dist/commonjs/v3/ai.js +2263 -178
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/chat.d.ts +23 -0
- package/dist/commonjs/v3/chat.js.map +1 -1
- package/dist/commonjs/v3/index.d.ts +2 -0
- package/dist/commonjs/v3/index.js +23 -17
- package/dist/commonjs/v3/index.js.map +1 -1
- package/dist/commonjs/v3/modelFileDownload.d.ts +10 -0
- package/dist/commonjs/v3/modelFileDownload.js +164 -0
- package/dist/commonjs/v3/modelFileDownload.js.map +1 -0
- package/dist/commonjs/v3/test/index.d.ts +2 -1
- package/dist/commonjs/v3/test/index.js +4 -1
- package/dist/commonjs/v3/test/index.js.map +1 -1
- package/dist/commonjs/v3/test/mock-channel-connector.d.ts +172 -0
- package/dist/commonjs/v3/test/mock-channel-connector.js +173 -0
- package/dist/commonjs/v3/test/mock-channel-connector.js.map +1 -0
- package/dist/commonjs/v3/test/mock-chat-agent.d.ts +81 -1
- package/dist/commonjs/v3/test/mock-chat-agent.js +137 -10
- package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/commonjs/v3/transcriptStorage.d.ts +44 -1
- package/dist/commonjs/v3/transcriptStorage.js +24 -0
- package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
- package/dist/commonjs/v3/webhookWaiters.d.ts +80 -0
- package/dist/commonjs/v3/webhookWaiters.js +156 -0
- package/dist/commonjs/v3/webhookWaiters.js.map +1 -0
- package/dist/commonjs/v3/webhooks.d.ts +178 -2
- package/dist/commonjs/v3/webhooks.js +322 -1
- package/dist/commonjs/v3/webhooks.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +560 -8
- package/dist/esm/v3/ai.js +2257 -178
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/chat.d.ts +23 -0
- package/dist/esm/v3/chat.js.map +1 -1
- package/dist/esm/v3/index.d.ts +2 -0
- package/dist/esm/v3/index.js +1 -0
- package/dist/esm/v3/index.js.map +1 -1
- package/dist/esm/v3/modelFileDownload.d.ts +10 -0
- package/dist/esm/v3/modelFileDownload.js +161 -0
- package/dist/esm/v3/modelFileDownload.js.map +1 -0
- package/dist/esm/v3/test/index.d.ts +2 -1
- package/dist/esm/v3/test/index.js +2 -1
- package/dist/esm/v3/test/index.js.map +1 -1
- package/dist/esm/v3/test/mock-channel-connector.d.ts +172 -0
- package/dist/esm/v3/test/mock-channel-connector.js +170 -0
- package/dist/esm/v3/test/mock-channel-connector.js.map +1 -0
- package/dist/esm/v3/test/mock-chat-agent.d.ts +81 -1
- package/dist/esm/v3/test/mock-chat-agent.js +137 -11
- package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
- package/dist/esm/v3/transcriptStorage.d.ts +44 -1
- package/dist/esm/v3/transcriptStorage.js +24 -0
- package/dist/esm/v3/transcriptStorage.js.map +1 -1
- package/dist/esm/v3/webhookWaiters.d.ts +80 -0
- package/dist/esm/v3/webhookWaiters.js +149 -0
- package/dist/esm/v3/webhookWaiters.js.map +1 -0
- package/dist/esm/v3/webhooks.d.ts +178 -2
- package/dist/esm/v3/webhooks.js +318 -1
- package/dist/esm/v3/webhooks.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/actions.mdx +6 -1
- package/docs/ai-chat/backend.mdx +34 -0
- package/docs/ai-chat/pending-messages.mdx +2 -0
- package/docs/ai-chat/reference.mdx +50 -2
- package/docs/mcp-tools.mdx +76 -0
- package/docs/webhooks/channels.mdx +128 -0
- package/docs/webhooks/connect.mdx +80 -0
- package/docs/webhooks/deliveries.mdx +92 -0
- package/docs/webhooks/filters.mdx +109 -0
- package/docs/webhooks/human-in-the-loop.mdx +149 -0
- package/docs/webhooks/linear.mdx +185 -0
- package/docs/webhooks/overview.mdx +99 -0
- package/docs/webhooks/session-routing.mdx +132 -0
- package/docs/webhooks/slack.mdx +360 -0
- package/docs/webhooks/sources.mdx +217 -0
- package/docs/webhooks/waiters.mdx +170 -0
- package/package.json +3 -2
|
@@ -47,7 +47,9 @@ Options for `chat.agent()`.
|
|
|
47
47
|
| `storage` | `TranscriptStorage` | `defaultStorage` | Where the conversation is persisted and read back. The platform snapshot by default; bring your own to write each change to your database. See [Transcript storage](/ai-chat/transcript-storage) |
|
|
48
48
|
| `hydrateMessages` | `(event: HydrateMessagesEvent) => UIMessage[] \| Promise<UIMessage[]>` | — | **Deprecated.** Load message history from backend, replacing the linear accumulator. Use `loadContext` on a `storage` instead; cannot be combined with `storage`. See [hydrateMessages](/ai-chat/lifecycle-hooks#hydratemessages) |
|
|
49
49
|
| `actionSchema` | `TaskSchema` | — | Schema for validating custom actions sent via `transport.sendAction()`. See [Actions](/ai-chat/actions) |
|
|
50
|
-
| `onAction` | `(event: ActionEvent) => Promise<void \| ActionTurn> \| void \| ActionTurn` | — | Handle custom actions. Actions are state edits: only `hydrateMessages` (or a storage's `loadContext`) + `onAction` fire. Return `chat.turn()` to run a turn on the edited history, or nothing for an edit only. See [Actions](/ai-chat/actions) |
|
|
50
|
+
| `onAction` | `(event: ActionEvent) => Promise<void \| ActionTurn> \| void \| ActionTurn` | — | Handle custom actions. Actions are state edits: only `hydrateMessages` (or a storage's `loadContext`) + `onAction` fire. Return `chat.turn()` to run a turn on the edited history (`chat.turn(message)` to answer a new user message), or nothing for an edit only. See [Actions](/ai-chat/actions) |
|
|
51
|
+
| `events` | `ChatEvent[]` | — | Webhook event descriptors (from `chat.event()`) whose verified deliveries are routed to this agent as actions and handled in `onAction`. See [session routing](/webhooks/session-routing). |
|
|
52
|
+
| `channels` | `ChannelConnector[]` | — | Channel connectors (for example `slack()`) that turn an external chat surface into a frontend for the agent: inbound messages run as turns and the reply posts back. See [channels](/webhooks/channels). |
|
|
51
53
|
| `onTurnStart` | `(event: TurnStartEvent) => Promise<void> \| void` | — | Fires every turn before `run()` |
|
|
52
54
|
| `onBeforeTurnComplete` | `(event: BeforeTurnCompleteEvent) => Promise<void> \| void` | — | Fires after response but before stream closes. Includes `writer`. |
|
|
53
55
|
| `onTurnComplete` | `(event: TurnCompleteEvent) => Promise<void> \| void` | — | Fires after each turn completes (stream closed) |
|
|
@@ -232,7 +234,7 @@ The persistence adapter set through `chat.agent({ storage })`. See [Transcript s
|
|
|
232
234
|
| Type | Shape |
|
|
233
235
|
| --- | --- |
|
|
234
236
|
| `TranscriptScope<TClientData>` | `{ chatId: string; clientData: TClientData }` |
|
|
235
|
-
| `TranscriptStorageContext<TClientData>` | `TranscriptScope` plus `{ turn: number; trigger: "submit-message" \| "regenerate-message" \| "action"; runId: string; ctx: TaskRunContext }` |
|
|
237
|
+
| `TranscriptStorageContext<TClientData>` | `TranscriptScope` plus `{ turn: number; trigger: "submit-message" \| "regenerate-message" \| "action" \| "action-turn"; source?: "client" \| "channel" \| "webhook"; connectorId?: string; runId: string; ctx: TaskRunContext }`. `action-turn` is the turn `chat.turn()` requested; `source` names the surface that drove the write |
|
|
236
238
|
| `TranscriptChange` | `{ op: "put"; message: UIMessage; final?: boolean }` \| `{ op: "remove"; id: string }` \| `{ op: "truncateAfter"; afterId: string }` \| `{ op: "state"; value: unknown \| null }` |
|
|
237
239
|
| `TranscriptChangeset` | `{ reason: "turn-complete" \| "turn-error" \| "action" \| "compaction" \| "recovery"; changes: TranscriptChange[]; transcript: TranscriptState; cursors?: TranscriptCursors }` |
|
|
238
240
|
| `TranscriptState` | `{ entries: Array<{ id: string; final: boolean; message: UIMessage }>; state: unknown \| null }`: the whole conversation after the changeset's changes, for stores that write one document |
|
|
@@ -275,6 +277,7 @@ Passed to the `onAction` callback. See [Actions](/ai-chat/actions).
|
|
|
275
277
|
| `clientData` | Typed by `clientDataSchema` | Custom data from the frontend |
|
|
276
278
|
| `uiMessages` | `UIMessage[]` | Accumulated UI messages (after hydration, if set) |
|
|
277
279
|
| `messages` | `ModelMessage[]` | Accumulated model messages (after hydration, if set) |
|
|
280
|
+
| `channel` | `{ connectorId, provider, event, deliveryId }` | For a channel action (a button the agent posted, such as a Slack `slackButton()`): the verified click event. Absent otherwise |
|
|
278
281
|
|
|
279
282
|
## TurnStartEvent
|
|
280
283
|
|
|
@@ -538,6 +541,8 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`.
|
|
|
538
541
|
| Method | Description |
|
|
539
542
|
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
540
543
|
| `chat.agent(options)` | Create a chat agent |
|
|
544
|
+
| `chat.event(options)` | Declare an inbound webhook event descriptor an agent claims via `chat.agent({ events })`. See [session routing](/webhooks/session-routing). |
|
|
545
|
+
| `chat.channels.custom(options)` | Create a generic chat-frontend channel over any verified webhook source (you supply the egress). The `slack()` preset ships in `@trigger.dev/slack`. See [channels](/webhooks/channels). |
|
|
541
546
|
| `chat.createSession(payload, options)` | Create an async iterator for chat turns |
|
|
542
547
|
| `chat.pipe(source, options?)` | Pipe a stream to the frontend (from anywhere inside a task) |
|
|
543
548
|
| `chat.pipeAndCapture(source, options?)` | Pipe and capture the response; returns `{ message, status, error }` |
|
|
@@ -570,6 +575,49 @@ All methods available on the `chat` object from `@trigger.dev/sdk/ai`.
|
|
|
570
575
|
| `chat.withUIMessage(config?)` | Returns a [ChatBuilder](/ai-chat/types#chatbuilder) with a fixed `UIMessage` subtype. See [Types](/ai-chat/types) |
|
|
571
576
|
| `chat.withClientData({ schema })` | Returns a [ChatBuilder](/ai-chat/types#chatbuilder) with a fixed client data schema. See [Types](/ai-chat/types#typed-client-data-with-chatwithclientdata) |
|
|
572
577
|
|
|
578
|
+
## `chat.event`
|
|
579
|
+
|
|
580
|
+
Declare an inbound webhook event that an agent claims via [`events`](#chatagentoptions) on `chat.agent()`. It is a descriptor only, with no handler: it names the [endpoint](/webhooks/overview) it subscribes to, a `key` template that resolves each delivery to a durable [session](/ai-chat/sessions), and an optional `type` label (defaults to the descriptor `id`). Verified deliveries are routed to that session and arrive at `onAction` as a `{ type, event, source, headers, deliveryId, endpoint }` envelope, not as a chat turn. See [session routing](/webhooks/session-routing).
|
|
581
|
+
|
|
582
|
+
```ts
|
|
583
|
+
import { webhooks } from "@trigger.dev/sdk";
|
|
584
|
+
import { chat } from "@trigger.dev/sdk/ai";
|
|
585
|
+
|
|
586
|
+
export const stripe = webhooks.endpoint.define({ id: "stripe", source: webhooks.stripe() });
|
|
587
|
+
|
|
588
|
+
export const orderEvents = chat.event({
|
|
589
|
+
id: "order-events",
|
|
590
|
+
endpoint: stripe,
|
|
591
|
+
key: "{event.data.object.customer}",
|
|
592
|
+
type: "order.event",
|
|
593
|
+
});
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
## `chat.channels.custom`
|
|
597
|
+
|
|
598
|
+
Create a generic chat-frontend channel on any [endpoint](/webhooks/overview), claimed via [`channels`](#chatagentoptions) on `chat.agent()`. You supply the session `key`, the `inbound` map from event to turn message, and your own `send` egress that posts the reply back, so the whole round-trip is under your control. Inbound messages run as normal turns and the reply is posted back. The `slack()` preset ships in `@trigger.dev/slack` and wires the egress for you. See [channels](/webhooks/channels), and the interactive approvals layer at [human-in-the-loop](/webhooks/human-in-the-loop).
|
|
599
|
+
|
|
600
|
+
```ts
|
|
601
|
+
import { webhooks } from "@trigger.dev/sdk";
|
|
602
|
+
import { chat } from "@trigger.dev/sdk/ai";
|
|
603
|
+
|
|
604
|
+
const surface = webhooks.endpoint.define({
|
|
605
|
+
id: "my-surface",
|
|
606
|
+
source: webhooks.custom<MyEvent>({ /* verifier config */ }),
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
export const mySurface = chat.channels.custom({
|
|
610
|
+
id: "my-surface",
|
|
611
|
+
endpoint: surface,
|
|
612
|
+
key: "{event.conversationId}",
|
|
613
|
+
inbound: (event) => event.text,
|
|
614
|
+
send: async (message, ctx) => {
|
|
615
|
+
const ref = await postToMySurface(ctx.event, message.text, ctx.previousRef);
|
|
616
|
+
return { ref };
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
```
|
|
620
|
+
|
|
573
621
|
## `chat.withUIMessage`
|
|
574
622
|
|
|
575
623
|
Returns a [`ChatBuilder`](/ai-chat/types#chatbuilder) with a fixed `UIMessage` subtype. Chain `.withClientData()`, hook methods, and `.agent()`.
|
package/docs/mcp-tools.mdx
CHANGED
|
@@ -331,3 +331,79 @@ Append one record to a named side channel's `in` stream. Sends control input to
|
|
|
331
331
|
<Callout type="warning">
|
|
332
332
|
`write_session_channel` is a write operation and is not available in readonly mode.
|
|
333
333
|
</Callout>
|
|
334
|
+
|
|
335
|
+
## Webhook Tools
|
|
336
|
+
|
|
337
|
+
Set up and debug [hosted webhook endpoints](/webhooks/overview). Endpoint ids start with `wh_` and delivery ids with `whd_`; endpoint tools also accept an endpoint's declared id. The same operations are on the [Webhooks API](/management/webhooks/list-endpoints).
|
|
338
|
+
|
|
339
|
+
### list_webhook_endpoints
|
|
340
|
+
|
|
341
|
+
List the environment's webhook endpoints: each one's id, declared id, source, status, URL and whether its signing secret is set.
|
|
342
|
+
|
|
343
|
+
**Example usage:**
|
|
344
|
+
- `"Which webhook endpoints do I have in dev?"`
|
|
345
|
+
|
|
346
|
+
### get_webhook_endpoint_details
|
|
347
|
+
|
|
348
|
+
Get an endpoint's subscribers, with the filters that decide which events each accepts, and its setup prompt: the steps for registering the endpoint and its signing secret with the provider.
|
|
349
|
+
|
|
350
|
+
**Parameters:**
|
|
351
|
+
- `endpointId` (required): the endpoint's declared id (such as `payments`) or `wh_` id
|
|
352
|
+
|
|
353
|
+
**Example usage:**
|
|
354
|
+
- `"Connect my payments webhook to Stripe"`
|
|
355
|
+
|
|
356
|
+
### list_webhook_deliveries
|
|
357
|
+
|
|
358
|
+
List recent deliveries, newest first.
|
|
359
|
+
|
|
360
|
+
**Parameters:**
|
|
361
|
+
- `endpointId` (optional): only this endpoint's deliveries, by declared id or `wh_` id
|
|
362
|
+
- `status` (optional): only these statuses, such as `["failed", "filtered"]`
|
|
363
|
+
- `period` (optional, default: `7d`): how far back to look
|
|
364
|
+
- `limit` (optional, default: `20`): how many to return, up to 100
|
|
365
|
+
- `cursor` (optional): the cursor from a previous call, for the next page
|
|
366
|
+
|
|
367
|
+
**Example usage:**
|
|
368
|
+
- `"Did the Stripe checkout event arrive in the last hour?"`
|
|
369
|
+
- `"Show me failed deliveries on the github endpoint"`
|
|
370
|
+
|
|
371
|
+
### get_webhook_delivery
|
|
372
|
+
|
|
373
|
+
Get one delivery: its status and error, each subscriber's result (the run or session it started, or why its filter skipped the event), how many waiting runs it resumed, and its event and headers.
|
|
374
|
+
|
|
375
|
+
**Parameters:**
|
|
376
|
+
- `deliveryId` (required): the delivery's `whd_` id
|
|
377
|
+
|
|
378
|
+
**Example usage:**
|
|
379
|
+
- `"Why didn't this webhook trigger my order task?"`
|
|
380
|
+
|
|
381
|
+
### replay_webhook_delivery
|
|
382
|
+
|
|
383
|
+
Replay a delivery from its stored event. Without `targetId` it goes to every subscriber again, with their filters checked again. With `targetId` it goes to that subscriber even if its filter skipped the event.
|
|
384
|
+
|
|
385
|
+
**Parameters:**
|
|
386
|
+
- `deliveryId` (required): the delivery's `whd_` id
|
|
387
|
+
- `targetId` (optional): the subscriber id to replay to
|
|
388
|
+
|
|
389
|
+
**Example usage:**
|
|
390
|
+
- `"Replay that delivery to the refunds task"`
|
|
391
|
+
|
|
392
|
+
### generate_webhook_secret
|
|
393
|
+
|
|
394
|
+
Generate a new signing secret for an endpoint and return it once, for providers where you choose the secret (such as GitHub). It replaces any existing secret.
|
|
395
|
+
|
|
396
|
+
**Parameters:**
|
|
397
|
+
- `endpointId` (required): the endpoint's declared id (such as `payments`) or `wh_` id
|
|
398
|
+
|
|
399
|
+
### set_webhook_secret
|
|
400
|
+
|
|
401
|
+
Store a signing secret or public key the provider issued, such as Stripe's `whsec_` secret. It replaces any existing secret.
|
|
402
|
+
|
|
403
|
+
**Parameters:**
|
|
404
|
+
- `endpointId` (required): the endpoint's declared id (such as `payments`) or `wh_` id
|
|
405
|
+
- `secret` (required): the secret or public key
|
|
406
|
+
|
|
407
|
+
<Callout type="warning">
|
|
408
|
+
`replay_webhook_delivery`, `generate_webhook_secret` and `set_webhook_secret` are write operations and are not available in readonly mode.
|
|
409
|
+
</Callout>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Channels (chat frontends)"
|
|
3
|
+
description: "Point Slack (or any chat surface) at an agent: messages become turns and replies post back."
|
|
4
|
+
sidebarTitle: "Channels"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
A [session route](/webhooks/session-routing) delivers a verified event to an agent as an [action](/ai-chat/actions): the agent reacts, and the response is a side effect. A **channel** is the other half: the webhook IS the chat surface. Inbound messages become **turns** (the normal `run()` loop), and the agent's reply is posted **back** to the surface. A Slack thread becomes a real conversation with the agent, exactly like the browser chat, just a different frontend.
|
|
8
|
+
|
|
9
|
+
List channels on a [`chat.agent`](/ai-chat/overview) alongside (or instead of) `events`:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { webhooks } from "@trigger.dev/sdk";
|
|
13
|
+
import { chat } from "@trigger.dev/sdk/ai";
|
|
14
|
+
import { slack, webhookSource } from "@trigger.dev/slack";
|
|
15
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
16
|
+
|
|
17
|
+
const slackEvents = webhooks.endpoint.define({ id: "slack", source: webhookSource() });
|
|
18
|
+
|
|
19
|
+
export const supportAgent = chat.agent({
|
|
20
|
+
id: "support-agent",
|
|
21
|
+
channels: [slack({ id: "support-slack", endpoint: slackEvents, token: process.env.SLACK_BOT_TOKEN! })],
|
|
22
|
+
run: async ({ messages, streamText }) =>
|
|
23
|
+
streamText({ model: anthropic("claude-sonnet-4-5"), messages }),
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The `run()` loop is unchanged: the agent does not know or care that it is talking to Slack. One verified Slack message in a thread is routed to a durable [session](/ai-chat/sessions) keyed to that thread, run as a turn, and the reply is posted into the thread.
|
|
28
|
+
|
|
29
|
+
## Slack
|
|
30
|
+
|
|
31
|
+
`slack()` from `@trigger.dev/slack` makes the agent a native Slack agent: the session shows its status, the reply streams into the thread with tool calls as task cards, approvals are buttons and Slack's stop button stops the turn. Shared images and PDFs reach the model, messages sent mid-reply can be folded into it, and the agent can post its own buttons. See [Slack agents](/webhooks/slack) for setup and options.
|
|
32
|
+
|
|
33
|
+
## Linear
|
|
34
|
+
|
|
35
|
+
`linear()` from `@trigger.dev/linear` makes the agent a Linear agent: assign it an issue or @mention it and it works the issue inside Linear's agent session UI, with thoughts, actions, a plan and approvals. See [Linear agents](/webhooks/linear).
|
|
36
|
+
|
|
37
|
+
## Approvals and interactive controls
|
|
38
|
+
|
|
39
|
+
An agent on a channel can pause a turn to get a human decision, approving a refund or confirming a deletion, and resume once someone clicks a button in the thread. `slack()` renders Approve and Deny buttons for you and collapses them to the decision once clicked; you can replace them, or give a tool its own UI whose click becomes the tool's result (see [Slack agents](/webhooks/slack#approvals-and-buttons)). See [human-in-the-loop](/webhooks/human-in-the-loop).
|
|
40
|
+
|
|
41
|
+
## Any surface: `chat.channels.custom`
|
|
42
|
+
|
|
43
|
+
For a surface without a preset, `chat.channels.custom` is the generic connector. You supply the [endpoint](/webhooks/overview) it subscribes to, the session `key`, the `inbound` map, and the egress `send`:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { chat } from "@trigger.dev/sdk/ai";
|
|
47
|
+
import { webhooks } from "@trigger.dev/sdk";
|
|
48
|
+
|
|
49
|
+
const surface = webhooks.endpoint.define({
|
|
50
|
+
id: "my-surface",
|
|
51
|
+
source: webhooks.custom<MyEvent>({ /* verifier config */ }),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const mySurface = chat.channels.custom({
|
|
55
|
+
id: "my-surface",
|
|
56
|
+
endpoint: surface,
|
|
57
|
+
key: "{event.conversationId}",
|
|
58
|
+
inbound: (e) => e.text,
|
|
59
|
+
outbound: (reply) => (reply.text ? { text: reply.text } : null), // null posts nothing
|
|
60
|
+
send: async (message, ctx) => {
|
|
61
|
+
const ref = await postToMySurface(ctx.event, message.text, ctx.previousRef);
|
|
62
|
+
return { ref }; // an existing ref means edit-in-place on the next turn
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`send` is called to post the reply. `ctx.previousRef` is the ref you returned last time, so streaming or a follow-up edits the same message instead of posting a new one. Return `null` from `outbound` to stay silent (a tool-only turn, say).
|
|
68
|
+
|
|
69
|
+
An event that `inbound` maps to an empty message (no text, and no file or data parts) runs no turn. A click or system event your surface sends to the same endpoint is dropped rather than answered with an empty prompt.
|
|
70
|
+
|
|
71
|
+
List the connector in `chat.agent({ channels: [mySurface] })`, which subscribes the agent to the endpoint with the id `agentId:connectorId`. Other subscribers on the endpoint keep receiving its deliveries.
|
|
72
|
+
|
|
73
|
+
## Stopping a turn from the channel
|
|
74
|
+
|
|
75
|
+
A connector can map an event to a control with `onControl`. Returning `{ kind: "stop" }` while a turn is running aborts it through the run's `stopSignal`, exactly like the browser's stop button: the partial reply posts with `stopped: true` and the event is consumed instead of running as a turn. `linear()` and `slack()` map their stop buttons this way; a custom connector might map a `/stop` command:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
chat.channels.custom({
|
|
79
|
+
// ...
|
|
80
|
+
onControl: (e) => (e.text.trim() === "/stop" ? { kind: "stop" } : null),
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
When nothing is running, the event is handled by `inbound` as an ordinary message. Return `whenIdle: "drop"` to consume it instead: no turn runs, and if the session is waiting on an [approval](/webhooks/human-in-the-loop) the pending tool calls are resolved as stopped and the connector's `cancelInteraction(cancelled, ctx)` takes the controls down (each cancelled call carries the `ref` its controls were posted with). Set `at` to when the user asked (epoch milliseconds), and a stop older than the latest turn is consumed without stopping it or cancelling that turn's approval, so a late or repeated delivery never stops the wrong turn. A stop pressed while the latest turn ran but delivered after it ended is consumed too, rather than handled as a message.
|
|
85
|
+
|
|
86
|
+
## Status and native streaming
|
|
87
|
+
|
|
88
|
+
Two more hooks let a connector mirror the session into a surface that has its own notion of an agent, the way `slack()` does:
|
|
89
|
+
|
|
90
|
+
- `lifecycle(state, ctx)` reports `working` when a turn starts (and every 50 minutes while it runs), `waiting` when it pauses on a human decision, `idle` when it ends for any reason, and `closed` on `chat.close()`. `ctx.firstTurn` and `ctx.prompt` are there to title a new conversation. Calls are serialized, so an older report never lands after a newer one.
|
|
91
|
+
- `stream(ctx)`, with `delivery: "stream"`, returns a per-turn transport `{ text(delta), progress?(event), end(outcome) }`. The runtime feeds it the reply's markdown deltas and tool progress, then calls `end({ stopped, pending, errorText? })` once. Resolve `{ delivered: true }` when the reply reached the surface, and the runtime does not post it again. Call `ctx.opened(ref)` once the surface has created the stream message: the ref is saved with the session while the turn runs, and if the worker dies mid-stream the next one calls the connector's `endInterruptedStream(ref, ctx)` before it answers, so the old stream does not look like it is still being written.
|
|
92
|
+
|
|
93
|
+
## Bound sessions
|
|
94
|
+
|
|
95
|
+
The first channel message binds the session to its connector and thread. From then on, any turn on that session posts its answer back through the same connector, including turns no channel delivery started: a [`chat.event`](/webhooks/session-routing#answering-into-a-channel) action that returns `chat.turn()`, or a message from a browser client sharing the session via [`chat.createWatchToken`](/ai-chat/reference). `slack()` binds to the team, channel, thread and the user it is answering; a custom connector opts in with `bind`:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
chat.channels.custom({
|
|
99
|
+
id: "my-surface",
|
|
100
|
+
endpoint: surface,
|
|
101
|
+
key: "{event.conversationId}",
|
|
102
|
+
inbound: (e) => ({ message: e.text, externalUserId: e.userId, externalMessageId: e.id }),
|
|
103
|
+
bind: (e) => ({ conversationId: e.conversationId }),
|
|
104
|
+
send: async (message, ctx) => {
|
|
105
|
+
// ctx.event is absent on a bound turn; ctx.handle always carries what bind() returned
|
|
106
|
+
const target = ctx.handle ?? { conversationId: ctx.event!.conversationId };
|
|
107
|
+
return { ref: await postToMySurface(target, message.text, ctx.previousRef) };
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`inbound` may return `{ message, externalUserId, externalMessageId }` as well as a plain string or `UIMessage`. The runtime stamps every channel-delivered user message with `metadata: { source: "channel", connectorId, provider, deliveryId, externalDeliveryId?, externalUserId?, externalMessageId? }` (`deliveryId` is the delivery's `whd_` id and `externalDeliveryId` the provider's), and a [transcript storage](/ai-chat/transcript-storage) receives `source: "channel"` and the `connectorId` on its save context, so a store you own can attribute and reconcile channel messages.
|
|
113
|
+
|
|
114
|
+
## Files, buttons and steering
|
|
115
|
+
|
|
116
|
+
Three more hooks cover what a chat surface carries besides text:
|
|
117
|
+
|
|
118
|
+
- `download(url, { handle })` fetches files only the surface's credentials can read. Have `inbound` return file parts on the surface's URLs, and the `streamText` passed to `run()` asks each connector's `download` for every file URL in the prompt (return null for URLs it doesn't own). Downloads are cached for the run and the transcript keeps the URL. A file `download` throws on becomes a short note in that turn's messages, and the turn still runs.
|
|
119
|
+
- `toAction(event)` maps a click on a control the agent posted to an action. It reaches `onAction`, validated against `actionSchema`, with the click itself as `channel.event`. Returning `chat.turn(message)` answers in the bound thread. Return null for anything else, such as a click answering a pending tool call, which `onInteraction` handles.
|
|
120
|
+
- With [`pendingMessages`](/ai-chat/pending-messages) on the agent, a channel message that arrives while a turn is running goes through `inbound` and is offered to `shouldInject` like a browser message (its `metadata.source` is `"channel"`). Stops, clicks and empty messages are never offered.
|
|
121
|
+
- `received(ctx)` acknowledges a channel message that arrives while a turn is running, as soon as it arrives and whether or not `pendingMessages` folds it in, so its wording should be true either way: `slack()` reacts with 👀, `linear()` posts a thought. Stops, clicks and empty messages are not acknowledged.
|
|
122
|
+
|
|
123
|
+
## Channels vs events
|
|
124
|
+
|
|
125
|
+
Both are inbound surfaces on a `chat.agent`, and an agent can list both:
|
|
126
|
+
|
|
127
|
+
- [`events`](/webhooks/session-routing) (`chat.event`): the webhook is a signal. Delivered to `onAction`; the agent acts, no reply is sent back.
|
|
128
|
+
- `channels` (`slack`, `chat.channels.custom`): the webhook is a chat frontend. Delivered as a turn to `run()`; the reply is posted back.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Connect a webhook provider"
|
|
3
|
+
description: "Register an endpoint's URL and signing secret with the provider, by hand or with an AI coding agent, then check that events arrive."
|
|
4
|
+
sidebarTitle: "Connect a provider"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Connect a provider to an [endpoint](/webhooks/overview) by registering the endpoint's URL with the provider and giving Trigger.dev the credential that verifies its requests.
|
|
8
|
+
|
|
9
|
+
Deploy the project or start `trigger dev` first. Once the CLI finds the endpoint, it appears on the environment's **Webhooks** page. The **Signing secret** column shows **Ready** when the endpoint can verify deliveries and **Needs secret** until then. Every delivery is rejected while an endpoint needs its secret.
|
|
10
|
+
|
|
11
|
+
## Set it up with an AI agent
|
|
12
|
+
|
|
13
|
+
Each endpoint has a setup prompt: the URL, the events its subscribers filter on, the provider's own setup steps, and how to generate or store the signing secret. Choose **Copy setup prompt** on the Webhooks page or the endpoint's page, and paste it into an AI coding agent such as Claude Code.
|
|
14
|
+
|
|
15
|
+
The prompt tells the agent to use the Trigger.dev MCP server (`npx trigger.dev@latest install-mcp`), which has these tools:
|
|
16
|
+
|
|
17
|
+
| Tool | What it does |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `list_webhook_endpoints` | Lists the environment's endpoints, with their URL and whether a secret is set. |
|
|
20
|
+
| `get_webhook_endpoint_details` | Reads one endpoint: URL, subscribers and their filters, secret state, and the setup prompt. |
|
|
21
|
+
| `generate_webhook_secret` | Generates a signing secret and returns it once, for providers where you choose the secret. |
|
|
22
|
+
| `set_webhook_secret` | Stores a secret the provider issued. |
|
|
23
|
+
|
|
24
|
+
Without the MCP server, the same operations are available on the REST API with the environment's secret key:
|
|
25
|
+
|
|
26
|
+
| Request | What it does |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| [`GET /api/v1/webhooks/endpoints`](/management/webhooks/list-endpoints) | Lists the environment's endpoints, with their URL and whether a secret is set. |
|
|
29
|
+
| [`GET /api/v1/webhooks/endpoints/{endpointId}`](/management/webhooks/retrieve-endpoint) | Reads the endpoint, including `subscribers` and `setupPrompt`. |
|
|
30
|
+
| [`POST /api/v1/webhooks/endpoints/{endpointId}/rotate-secret`](/management/webhooks/generate-secret) | Generates a secret and returns it once. |
|
|
31
|
+
| [`PUT /api/v1/webhooks/endpoints/{endpointId}/secret`](/management/webhooks/set-secret) | Stores a secret, sent as `{ "secret": "..." }`. |
|
|
32
|
+
|
|
33
|
+
`endpointId` is the endpoint's declared id (the `id` passed to `webhooks.endpoint.define`) or its `wh_` id, shown on its page and returned by `list_webhook_endpoints`:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
curl -X PUT "https://api.trigger.dev/api/v1/webhooks/endpoints/stripe/secret" \
|
|
37
|
+
-H "Authorization: Bearer $TRIGGER_SECRET_KEY" \
|
|
38
|
+
-H "Content-Type: application/json" \
|
|
39
|
+
-d '{"secret": "whsec_..."}'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Set it up by hand
|
|
43
|
+
|
|
44
|
+
<Steps>
|
|
45
|
+
<Step title="Copy the webhook URL">
|
|
46
|
+
Open the endpoint from the Webhooks page and copy the **Webhook URL**.
|
|
47
|
+
</Step>
|
|
48
|
+
<Step title="Configure the provider">
|
|
49
|
+
Add the URL as the webhook destination in your provider's dashboard and select the events your subscribers need. The provider's setup determines where the verification credential comes from:
|
|
50
|
+
|
|
51
|
+
| Provider setup | Action |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| The provider supplies a signing secret, as Stripe and Svix do | Copy the secret from the provider, then choose **Set signing secret** on the endpoint. |
|
|
54
|
+
| The provider accepts a secret you choose, as GitHub does | Choose **Generate secret** on the endpoint, then paste the value into the provider's webhook configuration. |
|
|
55
|
+
| The provider uses an asymmetric signature, as Discord does | Copy the application's public key from the provider and choose **Set public key** on the endpoint. |
|
|
56
|
+
|
|
57
|
+
Set the credential before sending a test event or asking the provider to validate the URL.
|
|
58
|
+
</Step>
|
|
59
|
+
</Steps>
|
|
60
|
+
|
|
61
|
+
## Verify the connection
|
|
62
|
+
|
|
63
|
+
Send a test event from your provider, or use the endpoint's **Test** tab. The event appears on the endpoint's **Deliveries** tab. Open it to see which subscribers it reached and follow the links to their runs; see [Deliveries and endpoints](/webhooks/deliveries) for the statuses.
|
|
64
|
+
|
|
65
|
+
If no delivery appears, check that the provider uses the copied URL and that both sides have the same signing secret or public key.
|
|
66
|
+
|
|
67
|
+
## Verify a WhatsApp endpoint URL
|
|
68
|
+
|
|
69
|
+
`webhooks.whatsapp()` also needs a verify token for Meta's URL check. On the endpoint, choose **Generate verify token** and paste the value into Meta's **Verify Token** field. Set Meta's App Secret as the signing secret, then validate the URL in Meta's dashboard.
|
|
70
|
+
|
|
71
|
+
The verify token answers the URL check. The signing secret verifies event deliveries. See [Sources and verification](/webhooks/sources#endpoint-validation) for the response behavior.
|
|
72
|
+
|
|
73
|
+
## Rotate a credential
|
|
74
|
+
|
|
75
|
+
Choose **Rotate signing secret** to replace a provider-supplied secret. For a secret you generate, choose **Regenerate secret**, copy the new value, and update the provider's configuration. Use **Rotate public key** when replacing an asymmetric provider's key.
|
|
76
|
+
|
|
77
|
+
<Warning>
|
|
78
|
+
Trigger.dev stores credentials encrypted and doesn't display saved values. Copy a generated secret
|
|
79
|
+
before closing the dialog. Deliveries signed with the old secret fail verification after rotation.
|
|
80
|
+
</Warning>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Deliveries and endpoints"
|
|
3
|
+
description: "Inspect webhook payloads, where each delivery went, and the runs it started or resumed."
|
|
4
|
+
sidebarTitle: "Deliveries and endpoints"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**A delivery records a verified webhook event and what happened to it at each subscriber.** Find deliveries on an endpoint's **Deliveries** tab. A run that a delivery triggered links back to it, and to its endpoint, from the run's **Detail** tab.
|
|
8
|
+
|
|
9
|
+
## Endpoints
|
|
10
|
+
|
|
11
|
+
The environment's **Webhooks** page lists its endpoints with their URL, whether the signing secret is set, the last 24 hours of activity, and the last delivery. Open an endpoint for its tabs:
|
|
12
|
+
|
|
13
|
+
| Tab | Shows |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| **Deliveries** | The endpoint's deliveries, filterable by status and time. |
|
|
16
|
+
| **Subscribers** | Every task, agent session and channel subscribed to the endpoint, with each one's filter. |
|
|
17
|
+
| **Waiters** | Runs currently [waiting](/webhooks/waiters) on the endpoint. |
|
|
18
|
+
| **Test** | Sends a sample event through the endpoint. |
|
|
19
|
+
|
|
20
|
+
The endpoint's details show its URL, source, verification scheme and signing secret actions. See [Connect a provider](/webhooks/connect).
|
|
21
|
+
|
|
22
|
+
### Disable an endpoint
|
|
23
|
+
|
|
24
|
+
Disable an endpoint to stop accepting its deliveries without removing it from your code. Its status becomes `inactive` and its URL responds with `404`, so the provider records failed deliveries and retries them on its own schedule:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
curl -X POST "https://api.trigger.dev/api/v1/webhooks/endpoints/stripe/disable" \
|
|
28
|
+
-H "Authorization: Bearer $TRIGGER_SECRET_KEY"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
[Enable](/management/webhooks/enable-endpoint) it again with `POST /api/v1/webhooks/endpoints/{endpointId}/enable`. An endpoint that is no longer in the environment's current deployment is also `inactive`.
|
|
32
|
+
|
|
33
|
+
## Delivery status
|
|
34
|
+
|
|
35
|
+
| Status | Meaning |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `PENDING` | Waiting to be routed, including after a retryable error at a subscriber. |
|
|
38
|
+
| `PROCESSING` | Being routed. A delivery that resumes many waiting runs stays here until they have all been resumed. |
|
|
39
|
+
| `SUCCEEDED` | At least one subscriber or waiting run received it, and nothing failed. |
|
|
40
|
+
| `FAILED` | A subscriber failed after its retries. The delivery's error names it. |
|
|
41
|
+
| `FILTERED` | No subscriber's filter matched, or the endpoint has no subscribers. The reason shows the failed clause. |
|
|
42
|
+
| `UNMATCHED` | Runs were waiting on the endpoint, but none matched and no subscriber took it. |
|
|
43
|
+
|
|
44
|
+
Delivery status tracks routing. Each run it started has its own status and retry history.
|
|
45
|
+
|
|
46
|
+
## Where a delivery went
|
|
47
|
+
|
|
48
|
+
Open a delivery to see its payload and headers, and a row per subscriber with its result (`SUCCEEDED`, `FILTERED` with the reason, or `FAILED` with the error) and a link to the run it started. Waiting runs are summarized in one row: how many waiters matched, how many were resumed, and how many failed to resume.
|
|
49
|
+
|
|
50
|
+
A subscriber that fails transiently is retried on its own; subscribers that already succeeded are not run again.
|
|
51
|
+
|
|
52
|
+
## Replay
|
|
53
|
+
|
|
54
|
+
Replaying a delivery routes its event again as a new delivery. Filters are checked again, so the replay reaches the subscribers that would take the event now:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
curl -X POST "https://api.trigger.dev/api/v1/webhooks/deliveries/whd_1234/replay" \
|
|
58
|
+
-H "Authorization: Bearer $TRIGGER_SECRET_KEY"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Add `?targetId=` to send it to one subscriber regardless of its filter. `targetId` is the subscriber id: a `webhook()` task's id, or `agentId:eventId` for an agent's `chat.event` or channel. A replay never resumes waiting runs.
|
|
62
|
+
|
|
63
|
+
## From an agent or the API
|
|
64
|
+
|
|
65
|
+
The [MCP server](/mcp-introduction) has `list_webhook_deliveries` (filter by endpoint, status and time), `get_webhook_delivery` (each subscriber's result, the waiter summary, the event and headers) and `replay_webhook_delivery`. The same data is on the REST API:
|
|
66
|
+
|
|
67
|
+
| Request | What it does |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| [`GET /api/v1/webhooks/deliveries`](/management/webhooks/list-deliveries) | Lists deliveries, newest first. Filter with `filter[endpoint]` (declared or `wh_` ids), `filter[status]`, and `filter[period]` or `filter[from]`/`filter[to]`. |
|
|
70
|
+
| [`GET /api/v1/webhooks/deliveries/{deliveryId}`](/management/webhooks/retrieve-delivery) | Reads a delivery, with a `targets` entry per subscriber and one for waiting runs. |
|
|
71
|
+
| [`POST /api/v1/webhooks/deliveries/{deliveryId}/replay`](/management/webhooks/replay-delivery) | Replays it, as above. The key needs permission to trigger every task the replay starts, and to write sessions if it reaches an agent's subscriber. |
|
|
72
|
+
|
|
73
|
+
## Deduplication
|
|
74
|
+
|
|
75
|
+
Trigger.dev uses the endpoint and idempotency key to deduplicate incoming events. Delivery records and task runs have separate deduplication windows:
|
|
76
|
+
|
|
77
|
+
| What is deduplicated | Window |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| Delivery records | Six hours by default. |
|
|
80
|
+
| Task runs | Three days for Stripe; one day for other sources. |
|
|
81
|
+
|
|
82
|
+
A retry after the delivery window expires can create another delivery record. If the task-run idempotency window is still active, routing returns the existing run instead of starting another one.
|
|
83
|
+
|
|
84
|
+
When the source maps a provider event ID, that ID becomes the key. For example, Stripe uses the event body's `id`, and GitHub uses the `X-GitHub-Delivery` header.
|
|
85
|
+
|
|
86
|
+
If the source doesn't map an event ID, the key is derived from the raw body, timestamp, and signature. A retry with a different timestamp or signature can therefore produce a different key. For a [custom source](/webhooks/sources#custom-providers), set `idempotencyField` to a stable event identifier when the provider supplies one.
|
|
87
|
+
|
|
88
|
+
## Verification and recording
|
|
89
|
+
|
|
90
|
+
Requests must pass the source's verification checks before they are recorded. Timestamp checks apply when configured by the source. A verification failure returns the source's rejected HTTP status (`400` by default) and creates no delivery or run.
|
|
91
|
+
|
|
92
|
+
Provider handshakes receive their configured response without creating a delivery. Other verified events are checked for duplicates, recorded, and routed. Filtered and unmatched deliveries stay visible, so you can see what arrived and why nothing ran.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Filtering deliveries"
|
|
3
|
+
description: "Choose which verified webhook events reach each subscriber, by payload fields, request headers, or endpoint metadata."
|
|
4
|
+
sidebarTitle: "Filters"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**A filter selects which verified deliveries reach a subscriber.** Each subscriber on an [endpoint](/webhooks/overview) has its own filter, so one delivery can start one task and skip another. A delivery no subscriber takes is recorded with status `FILTERED`, so you can inspect it in the dashboard.
|
|
8
|
+
|
|
9
|
+
## Add a filter
|
|
10
|
+
|
|
11
|
+
Set `filter` on the subscriber. This example accepts Stripe payment-intent events whose amount is at least 10,000:
|
|
12
|
+
|
|
13
|
+
```ts /trigger/payment-events.ts
|
|
14
|
+
import { webhook, webhooks } from "@trigger.dev/sdk";
|
|
15
|
+
|
|
16
|
+
export const stripe = webhooks.endpoint.define({ id: "stripe", source: webhooks.stripe() });
|
|
17
|
+
|
|
18
|
+
export const largePayments = webhook({
|
|
19
|
+
id: "large-payments",
|
|
20
|
+
endpoint: stripe,
|
|
21
|
+
filter: "event.type == 'payment_intent.succeeded' && event.data.object.amount >= 10000",
|
|
22
|
+
onEvent: async ({ event }) => {
|
|
23
|
+
console.log("Matched payment event", event.id);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The same `filter` works on a [`chat.event`](/webhooks/session-routing#filtering), a channel connector, and a [waiter](/webhooks/waiters), where it is checked after the waiter's `match`.
|
|
29
|
+
|
|
30
|
+
The filter is checked against the [endpoint's event type](/webhooks/sources#typing-the-event). Concrete types catch missing fields and incompatible literals. Fields typed as `unknown`, including values in Stripe's default `data.object`, allow arbitrary non-empty field paths. Operator literals and path structure are still checked.
|
|
31
|
+
|
|
32
|
+
## Check the result
|
|
33
|
+
|
|
34
|
+
Send an event and open its [delivery](/webhooks/deliveries) in the dashboard. Each subscriber's row shows whether it took the event, and a filtered subscriber shows the failed clause and its value. The provider receives the source's accepted HTTP status (`200` by default) either way.
|
|
35
|
+
|
|
36
|
+
<Note>
|
|
37
|
+
If filter evaluation throws an exception, Trigger.dev routes the event. Filters aren't a substitute
|
|
38
|
+
for signature verification or validation inside your handler.
|
|
39
|
+
</Note>
|
|
40
|
+
|
|
41
|
+
## The expression language
|
|
42
|
+
|
|
43
|
+
A filter is one or more `path operator value` clauses combined with `&&` and `||` (use parentheses to group).
|
|
44
|
+
|
|
45
|
+
### Paths
|
|
46
|
+
|
|
47
|
+
A path reads from one of three namespaces:
|
|
48
|
+
|
|
49
|
+
- `event.*`: the verified, parsed request body, for example `event.data.object.amount`.
|
|
50
|
+
- `header.*`: an inbound request header, matched case-insensitively, for example `header.x-github-event`.
|
|
51
|
+
- `webhook.*`: endpoint and delivery metadata: `source`, `id`, `deliveryId`, `externalRef`, and `tenantId`. The last two depend on the endpoint configuration.
|
|
52
|
+
|
|
53
|
+
Paths must have non-empty segments. `event.data.amount` is valid; `event.`, `event..amount`, and `event.amount.` are rejected.
|
|
54
|
+
|
|
55
|
+
[Session routing](/webhooks/session-routing) keys use the same namespaces in braces (`{event.data.object.customer}`), and [waiter](/webhooks/waiters) `match` paths use them as keys (`"event.data.object.metadata.orderId"`).
|
|
56
|
+
|
|
57
|
+
### Operators
|
|
58
|
+
|
|
59
|
+
| Operator | Meaning |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `==` `!=` | equality |
|
|
62
|
+
| `>` `<` `>=` `<=` | numeric comparison |
|
|
63
|
+
| `in` `not in` | membership in a list, for example `event.type in ['a', 'b']` |
|
|
64
|
+
| `startsWith` `endsWith` `contains` | string matching |
|
|
65
|
+
|
|
66
|
+
Values are strings in single quotes (`'created'`), numbers (`10000`), booleans (`true`), or `null`. Use a list of these values with `in` and `not in`.
|
|
67
|
+
|
|
68
|
+
### Comparing two fields
|
|
69
|
+
|
|
70
|
+
Equality and comparison operators also accept a field path on the right-hand side:
|
|
71
|
+
|
|
72
|
+
```text Filter expression
|
|
73
|
+
event.billing.country == event.shipping.country
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Matching inside a list
|
|
77
|
+
|
|
78
|
+
`any` matches if at least one array element meets the condition. `all` requires every element to match. The path inside the parentheses is relative to each element:
|
|
79
|
+
|
|
80
|
+
```text Filter expression
|
|
81
|
+
event.items any ( quantity > 0 )
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Spacing
|
|
85
|
+
|
|
86
|
+
Separate paths, operators, and values with one space, and put spaces inside grouping and quantifier parentheses, as in `( event.amount > 0 )` and `any ( quantity > 0 )`. Lists and quoted strings may contain spaces (`['opened', 'closed']`, `'Fix the build'`). These forms work with both the TypeScript validator and the deployment parser.
|
|
87
|
+
|
|
88
|
+
## Filter by event type
|
|
89
|
+
|
|
90
|
+
Use the field or header your provider supplies for its event type. GitHub uses the `x-github-event` header:
|
|
91
|
+
|
|
92
|
+
```ts /trigger/github-events.ts
|
|
93
|
+
import { webhook, webhooks } from "@trigger.dev/sdk";
|
|
94
|
+
|
|
95
|
+
export const github = webhooks.endpoint.define({ id: "github", source: webhooks.github() });
|
|
96
|
+
|
|
97
|
+
export const onIssuesAndPullRequests = webhook({
|
|
98
|
+
id: "github-issues-and-prs",
|
|
99
|
+
endpoint: github,
|
|
100
|
+
filter: "header.x-github-event in ['issues', 'pull_request']",
|
|
101
|
+
onEvent: async ({ headers }) => {
|
|
102
|
+
console.log("Matched GitHub event", headers.get("x-github-event"));
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Filtering a session route
|
|
108
|
+
|
|
109
|
+
A `filter` works the same way on [`chat.event`](/webhooks/session-routing): a delivery that doesn't match never reaches the session.
|