@trigger.dev/sdk 4.8.0-webhooks.20261015065339 → 4.8.0-webhooks.20261020092920

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.
Files changed (53) hide show
  1. package/dist/commonjs/v3/ai.d.ts +67 -1
  2. package/dist/commonjs/v3/ai.js +102 -29
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/query.d.ts +434 -1
  5. package/dist/commonjs/v3/query.js +115 -0
  6. package/dist/commonjs/v3/query.js.map +1 -1
  7. package/dist/commonjs/v3/webhooks.d.ts +8 -2
  8. package/dist/commonjs/v3/webhooks.js +2 -1
  9. package/dist/commonjs/v3/webhooks.js.map +1 -1
  10. package/dist/commonjs/version.js +1 -1
  11. package/dist/esm/v3/ai.d.ts +67 -1
  12. package/dist/esm/v3/ai.js +101 -28
  13. package/dist/esm/v3/ai.js.map +1 -1
  14. package/dist/esm/v3/query.d.ts +434 -1
  15. package/dist/esm/v3/query.js +115 -0
  16. package/dist/esm/v3/query.js.map +1 -1
  17. package/dist/esm/v3/webhooks.d.ts +8 -2
  18. package/dist/esm/v3/webhooks.js +2 -1
  19. package/dist/esm/v3/webhooks.js.map +1 -1
  20. package/dist/esm/version.js +1 -1
  21. package/docs/ai-chat/backend.mdx +6 -0
  22. package/docs/ai-chat/patterns/redacting-telemetry.mdx +117 -0
  23. package/docs/ai-chat/pending-messages.mdx +1 -1
  24. package/docs/ai-chat/reference.mdx +7 -4
  25. package/docs/ai-chat/transcript-storage.mdx +1 -1
  26. package/docs/apikeys.mdx +6 -4
  27. package/docs/cli-runs-commands.mdx +2 -2
  28. package/docs/cli-tasks-commands.mdx +95 -0
  29. package/docs/config/config-file.mdx +58 -0
  30. package/docs/config/extensions/prismaExtension.mdx +4 -4
  31. package/docs/deploy-environment-variables.mdx +4 -0
  32. package/docs/deployment/atomic-deployment.mdx +3 -3
  33. package/docs/deployment/overview.mdx +1 -1
  34. package/docs/deployment/preview-branches.mdx +1 -1
  35. package/docs/github-actions.mdx +3 -3
  36. package/docs/limits.mdx +6 -0
  37. package/docs/manual-setup.mdx +1 -1
  38. package/docs/mcp-tools.mdx +23 -3
  39. package/docs/observability/dashboards.mdx +62 -0
  40. package/docs/runs/max-duration.mdx +4 -0
  41. package/docs/self-hosting/env/supervisor.mdx +1 -0
  42. package/docs/webhooks/channels.mdx +6 -2
  43. package/docs/webhooks/connect.mdx +4 -0
  44. package/docs/webhooks/deliveries.mdx +4 -0
  45. package/docs/webhooks/filters.mdx +35 -2
  46. package/docs/webhooks/human-in-the-loop.mdx +6 -0
  47. package/docs/webhooks/linear.mdx +7 -3
  48. package/docs/webhooks/overview.mdx +4 -0
  49. package/docs/webhooks/session-routing.mdx +7 -1
  50. package/docs/webhooks/slack.mdx +12 -4
  51. package/docs/webhooks/sources.mdx +4 -0
  52. package/docs/webhooks/waiters.mdx +26 -2
  53. package/package.json +3 -2
@@ -45,6 +45,68 @@ You can also filter the data by:
45
45
  4. For charts you write [TRQL queries](/observability/query) and choose a visualization type.
46
46
  5. You can resize and reposition widgets on your dashboards.
47
47
 
48
+ ## Managing dashboards with the SDK
49
+
50
+ Custom dashboards can also be created and edited from code, so you can keep them in your repository and set them up from CI or a script. `query.dashboards` covers the same operations as the dashboard UI.
51
+
52
+ ```ts scripts/dashboards.ts
53
+ import { query } from "@trigger.dev/sdk";
54
+
55
+ const dashboard = await query.dashboards.create({
56
+ title: "Email tasks",
57
+ widgets: [
58
+ { title: "Overview", display: { type: "title" } },
59
+ {
60
+ title: "Total runs",
61
+ query: "SELECT count() AS total FROM runs WHERE task_identifier = 'send-email'",
62
+ display: { type: "bignumber", column: "total", aggregation: "sum" },
63
+ },
64
+ {
65
+ title: "Runs per day",
66
+ query:
67
+ "SELECT timeBucket(), count() AS runs FROM runs WHERE task_identifier = 'send-email' GROUP BY timeBucket ORDER BY timeBucket",
68
+ display: {
69
+ type: "chart",
70
+ chartType: "bar",
71
+ xAxisColumn: "timebucket",
72
+ yAxisColumns: ["runs"],
73
+ groupByColumn: null,
74
+ stacked: false,
75
+ sortByColumn: null,
76
+ sortDirection: "asc",
77
+ aggregation: "sum",
78
+ },
79
+ },
80
+ ],
81
+ });
82
+
83
+ console.log(dashboard.id); // dashboard_...
84
+ ```
85
+
86
+ Widgets without a `position` are stacked full width in the order you list them. Pass `position: { x, y, w, h }` on a 12-column grid to place a widget yourself.
87
+
88
+ To keep a dashboard in sync with code, replace all of its widgets in one call. Widgets you keep, matched by `id`, stay where they are unless you pass a new `position`:
89
+
90
+ ```ts scripts/dashboards.ts
91
+ const current = await query.dashboards.retrieve(dashboard.id);
92
+
93
+ await query.dashboards.replace(dashboard.id, {
94
+ widgets: [
95
+ {
96
+ id: "failures",
97
+ title: "Failed runs",
98
+ query: "SELECT count() AS failed FROM runs WHERE status = 'Failed'",
99
+ display: { type: "bignumber", column: "failed", aggregation: "sum" },
100
+ },
101
+ ],
102
+ revision: current.revision,
103
+ });
104
+ ```
105
+
106
+ `revision` is optional. When you pass the revision you last read, the write fails with a 409 if someone changed the dashboard in the meantime, instead of overwriting their change.
107
+
108
+ Every widget query is checked against the queryable tables when you save it, so a typo in a table or column name fails the request with the list of valid columns. `query.dashboards.widgets.add`, `widgets.update` and `widgets.del` change one widget at a time. Creating a dashboard or widget past your plan's limits returns a 403. See the [Query API reference](/management/query/create-dashboard) for every endpoint.
109
+
48
110
  ## Performance considerations
49
111
 
50
112
  ### Optimize queries for metrics
@@ -4,6 +4,10 @@ sidebarTitle: "Max duration"
4
4
  description: "Set a maximum duration for a task to run."
5
5
  ---
6
6
 
7
+ Runs have no maximum duration. You set a default `maxDuration` for your project and can override it per task or per run, or set it to `timeout.None` to let a run execute indefinitely.
8
+
9
+ Trigger is built for long running AI agents and workflows and we have seen Trigger.dev users with runs lasting more than six months.
10
+
7
11
  The `maxDuration` parameter sets a maximum compute time limit for tasks. When a task exceeds this duration, it will be automatically stopped. This helps prevent runaway tasks and manage compute resources effectively.
8
12
 
9
13
  You must set a default maxDuration in your `trigger.config.ts` file, which will apply to all tasks unless overridden:
@@ -65,6 +65,7 @@ mode: "wide"
65
65
  | `FAILED_POD_HANDLER_ENABLED` | No | true | Enable failed pod handler. |
66
66
  | `FAILED_POD_HANDLER_RECONNECT_INTERVAL_MS` | No | 1000 (1s) | Failed pod handler reconnect interval (ms). |
67
67
  | `FAILED_POD_HANDLER_WATCH_TIMEOUT_SECONDS` | No | 300 (5m) | How long the server keeps each failed pod watch open (s). |
68
+ | `RUNNER_KILL_REPORTS_ENABLED` | No | true | Fail a run straight away when its pod is killed, as out of memory only if Kubernetes reports `OOMKilled`. |
68
69
  | **Debug** | | | |
69
70
  | `DEBUG` | No | false | Enable debug logs. |
70
71
  | `SEND_RUN_DEBUG_LOGS` | No | false | Send run debug logs to the platform. |
@@ -4,6 +4,10 @@ description: "Point Slack (or any chat surface) at an agent: messages become tur
4
4
  sidebarTitle: "Channels"
5
5
  ---
6
6
 
7
+ import WebhooksBetaBanner from "/snippets/webhooks-beta-banner.mdx";
8
+
9
+ <WebhooksBetaBanner />
10
+
7
11
  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
12
 
9
13
  List channels on a [`chat.agent`](/ai-chat/overview) alongside (or instead of) `events`:
@@ -59,12 +63,12 @@ const mySurface = chat.channels.custom({
59
63
  outbound: (reply) => (reply.text ? { text: reply.text } : null), // null posts nothing
60
64
  send: async (message, ctx) => {
61
65
  const ref = await postToMySurface(ctx.event, message.text, ctx.previousRef);
62
- return { ref }; // an existing ref means edit-in-place on the next turn
66
+ return { ref }; // later sends in the same turn get it back as ctx.previousRef
63
67
  },
64
68
  });
65
69
  ```
66
70
 
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).
71
+ `send` is called to post the reply. Within a turn, `ctx.previousRef` is the ref you returned from the turn's earlier `send` (the acknowledgement, or the message being streamed), so later sends edit that message instead of posting a new one. Each turn starts without a ref: the next turn's answer is a new message. Return `null` from `outbound` to stay silent (a tool-only turn, say).
68
72
 
69
73
  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
74
 
@@ -4,6 +4,10 @@ description: "Register an endpoint's URL and signing secret with the provider, b
4
4
  sidebarTitle: "Connect a provider"
5
5
  ---
6
6
 
7
+ import WebhooksBetaBanner from "/snippets/webhooks-beta-banner.mdx";
8
+
9
+ <WebhooksBetaBanner />
10
+
7
11
  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
12
 
9
13
  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.
@@ -4,6 +4,10 @@ description: "Inspect webhook payloads, where each delivery went, and the runs i
4
4
  sidebarTitle: "Deliveries and endpoints"
5
5
  ---
6
6
 
7
+ import WebhooksBetaBanner from "/snippets/webhooks-beta-banner.mdx";
8
+
9
+ <WebhooksBetaBanner />
10
+
7
11
  **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
12
 
9
13
  ## Endpoints
@@ -4,6 +4,10 @@ description: "Choose which verified webhook events reach each subscriber, by pay
4
4
  sidebarTitle: "Filters"
5
5
  ---
6
6
 
7
+ import WebhooksBetaBanner from "/snippets/webhooks-beta-banner.mdx";
8
+
9
+ <WebhooksBetaBanner />
10
+
7
11
  **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
12
 
9
13
  ## Add a filter
@@ -27,7 +31,36 @@ export const largePayments = webhook({
27
31
 
28
32
  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
33
 
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.
34
+ 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. A filter built at runtime (typed as `string` rather than a string literal) skips the compile-time check, and is validated when the endpoint is deployed.
35
+
36
+ ## Narrow a union event
37
+
38
+ When the endpoint's event is a union, such as `webhooks.stripe<Stripe.Event>()`, an `event.<field> == '<value>'` clause selects the members whose field can hold that value. The rest of that `&&` chain is checked against those members, and the handler's `event` has their type:
39
+
40
+ ```ts /trigger/checkouts.ts
41
+ import { webhook, webhooks } from "@trigger.dev/sdk";
42
+ import type Stripe from "stripe";
43
+
44
+ export const stripe = webhooks.endpoint.define({
45
+ id: "stripe",
46
+ source: webhooks.stripe<Stripe.Event>(),
47
+ });
48
+
49
+ export const checkouts = webhook({
50
+ id: "checkouts",
51
+ endpoint: stripe,
52
+ filter: "event.type == 'checkout.session.completed' && event.data.object.amount_total >= 100",
53
+ onEvent: async ({ event }) => {
54
+ const session: Stripe.Checkout.Session = event.data.object; // no cast needed
55
+ console.log("Checkout completed", session.id);
56
+ },
57
+ });
58
+ ```
59
+
60
+ - `in [...]` selects the members for any listed value, and `||` gives the union of its branches. Parentheses group branches as usual.
61
+ - `!=`, `not in`, numeric comparisons, string operators, and `any`/`all` don't narrow. Neither do `null`, `'null'`, or `0`, because the server's `==` also matches a null field for those values.
62
+ - Without a type clause, a field that only some members have is accepted: an event without it never matches.
63
+ - The narrowed type also applies to the task's trigger payload, a `chat.event` action's `event` and `key`, channel connector callbacks, and a waiter's event. A filter built at runtime doesn't narrow.
31
64
 
32
65
  ## Check the result
33
66
 
@@ -83,7 +116,7 @@ event.items any ( quantity > 0 )
83
116
 
84
117
  ### Spacing
85
118
 
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.
119
+ Separate paths, operators, and values with one space. Parentheses may touch what they enclose, 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
120
 
88
121
  ## Filter by event type
89
122
 
@@ -4,6 +4,10 @@ sidebarTitle: "Human-in-the-loop"
4
4
  description: "Pause an agent mid-turn to get a human decision from the channel (Slack approve/deny), then resume with the answer."
5
5
  ---
6
6
 
7
+ import WebhooksBetaBanner from "/snippets/webhooks-beta-banner.mdx";
8
+
9
+ <WebhooksBetaBanner />
10
+
7
11
  **An agent on a [channel](/webhooks/channels) can pause a turn to get a human decision, then resume once someone answers in the thread.** It posts controls into the conversation and picks up where it left off once someone clicks, with the decision merged into the turn. This is the channel counterpart of browser [human-in-the-loop](/ai-chat/patterns/human-in-the-loop): the same no-`execute` tool, but the controls live in Slack (or your own surface) instead of a React component. The [`slack()`](/webhooks/slack) connector ships Approve and Deny buttons out of the box.
8
12
 
9
13
  ## How it works
@@ -115,7 +119,9 @@ chat.channels.custom({
115
119
  text: `Approve: ${pending[0].input.action}?`,
116
120
  buttons: pending.map((c) => [`${c.toolCallId}:yes`, `${c.toolCallId}:no`]),
117
121
  }),
122
+ // called for every delivery: return null for anything that isn't one of these button clicks
118
123
  onInteraction: (e) => {
124
+ if (typeof e.buttonValue !== "string" || !e.buttonValue.includes(":")) return null;
119
125
  const [toolCallId, choice] = e.buttonValue.split(":");
120
126
  return { toolCallId, output: { approved: choice === "yes" } };
121
127
  },
@@ -4,6 +4,10 @@ description: "Turn a chat.agent into a Linear agent: assign it an issue or @ment
4
4
  sidebarTitle: "Linear agents"
5
5
  ---
6
6
 
7
+ import WebhooksBetaBanner from "/snippets/webhooks-beta-banner.mdx";
8
+
9
+ <WebhooksBetaBanner />
10
+
7
11
  Linear opens an **agent session** when someone @mentions your app in an issue or delegates (assigns) an issue to it. `linear()` from `@trigger.dev/linear` is a [channel](/webhooks/channels) connector for those sessions: each one becomes a durable Trigger.dev [session](/ai-chat/sessions), the user's prompt runs as a turn, and the agent's work shows up in Linear as thoughts, actions, a plan, an approval card when it needs a decision, and a response when it is done.
8
12
 
9
13
  ```ts
@@ -44,7 +48,7 @@ The `run()` loop is the same one a browser chat or a Slack thread uses. Verifica
44
48
 
45
49
  ## What the agent sees
46
50
 
47
- On the first turn of a session the connector frames Linear's payload for the model: who summoned it and on which issue, the workspace **guidance** rules for agents, Linear's formatted **issue context** (title, description, labels, project, comments), and the comment that mentioned it. An assignment with no comment reads as "assigned ENG-412 to you". Prior comments on the issue are [seeded](/webhooks/channels#joining-a-thread-that-already-has-messages) as transcript messages, so the agent can answer with the context a reader of the issue has.
51
+ On the first turn of a session the connector frames Linear's payload for the model: who summoned it and on which issue, the workspace **guidance** rules for agents, Linear's formatted **issue context** (title, description, labels, project, comments), and the comment that mentioned it. An assignment with no comment reads as "assigned ENG-412 to you". Prior comments on the issue are seeded as transcript messages, so the agent can answer with the context a reader of the issue has.
48
52
 
49
53
  A follow-up prompt on the session is just the message. If the user hits **Stop** in Linear, see [Stopping](#stopping).
50
54
 
@@ -164,8 +168,8 @@ The first Linear turn binds the session to the agent session ([bound sessions](/
164
168
  | `endpoint` | endpoint | The Linear endpoint, declared with `webhooks.endpoint.define({ id, source: webhookSource() })`. Other `webhook()` tasks can subscribe to the same endpoint. |
165
169
  | `token` | `string` or resolver | The app's OAuth access token, or a function of the event (keyed on `organizationId`) for multi-workspace installs. |
166
170
  | `key` | `string` | Session [key](/webhooks/session-routing) template over `{event.*}`, `{header.*}` and `{webhook.*}`. Defaults to one session per Linear agent session. |
167
- | `filter` | `string` | Extra [filter](/webhooks/filters), composed with the built-in agent-session guard. |
168
- | `startOn` | `string` | Only start a session when the event matches. Existing sessions always resume. |
171
+ | `filter` | filter string | Extra [filter](/webhooks/filters), composed with the built-in agent-session guard. Checked against the endpoint's event type, like `webhook()`'s `filter`. |
172
+ | `startOn` | filter string | Only start a session when the event matches. Existing sessions always resume. Checked against the endpoint's event type. |
169
173
  | `context` | `{ guidance?, promptContext?, previousComments? }` | What from Linear's payload is put in front of the model on the first turn. All on by default. |
170
174
  | `progress` | `"actions"` or `"off"` | Mirror tool calls as action activities (default) or post only the response. |
171
175
  | `summarize` | `boolean` | Set the session title from each response's first line. Off by default; Linear titles the session itself. |
@@ -4,6 +4,10 @@ description: "Receive provider webhooks on hosted, verified endpoints and route
4
4
  sidebarTitle: "Overview"
5
5
  ---
6
6
 
7
+ import WebhooksBetaBanner from "/snippets/webhooks-beta-banner.mdx";
8
+
9
+ <WebhooksBetaBanner />
10
+
7
11
  **A webhook endpoint is one hosted URL that you register with a provider.** Trigger.dev verifies every request against the endpoint's [source](/webhooks/sources), records it as a [delivery](/webhooks/deliveries), and hands the event to everything subscribed to the endpoint.
8
12
 
9
13
  Declare the endpoint once, then subscribe to it:
@@ -4,6 +4,10 @@ description: "Route verified webhook deliveries to a durable per-key session ins
4
4
  sidebarTitle: "Session routing"
5
5
  ---
6
6
 
7
+ import WebhooksBetaBanner from "/snippets/webhooks-beta-banner.mdx";
8
+
9
+ <WebhooksBetaBanner />
10
+
7
11
  A plain [`webhook()`](/webhooks/overview) runs a fresh, stateless run for every delivery. Sometimes you want the opposite: deliveries that share a key (a customer, an installation, an issue) should land on **one durable [session](/ai-chat/sessions)** and be handled in order, with state carried across them. That is what session routing does.
8
12
 
9
13
  Instead of a handler, you declare a `chat.event` on an [endpoint](/webhooks/overview) and list it on an agent. The verified delivery is routed to a find-or-created session and arrives as an [action](/ai-chat/actions).
@@ -23,6 +27,8 @@ export const orderEvents = chat.event({
23
27
  endpoint: stripe,
24
28
  // one session per customer
25
29
  key: "{event.data.object.customer}",
30
+ // invoices always carry a customer; events without one would have no session to go to
31
+ filter: "event.type startsWith 'invoice.'",
26
32
  type: "order.event",
27
33
  });
28
34
  ```
@@ -113,7 +119,7 @@ A bound turn posts the final answer as a fresh message in the thread (no placeho
113
119
 
114
120
  Listing a descriptor on an agent subscribes that agent to the descriptor's endpoint, with the subscriber id `agentId:eventId`. If two agents list the same descriptor, the endpoint has two subscribers and every delivery goes to both, on the one URL the provider already knows.
115
121
 
116
- A session belongs to the agent that created it. When two agents compute the same key, the second agent's delivery to that session fails with an error naming the owner, so give each agent a key that can't collide, for example by prefixing `{webhook.id}` or a constant.
122
+ A session belongs to the agent that created it. When two agents compute the same key, the second agent's delivery to that session fails with an error naming the owner. Both agents see the same endpoint, so a `{webhook.*}` placeholder resolves the same for each and can't separate them. Give each agent its own descriptor with a constant prefix in the key instead, for example `"billing-{event.data.object.customer}"` and `"support-{event.data.object.customer}"`.
117
123
 
118
124
  An exported `chat.event` that no agent lists routes nothing. `trigger dev` warns about it (it is not an error, since declaring the descriptor before wiring the agent is a normal step), so wire it onto an agent once you are ready.
119
125
 
@@ -4,6 +4,10 @@ description: "Turn a chat.agent into a Slack agent: users message it in Slack, i
4
4
  sidebarTitle: "Slack agents"
5
5
  ---
6
6
 
7
+ import WebhooksBetaBanner from "/snippets/webhooks-beta-banner.mdx";
8
+
9
+ <WebhooksBetaBanner />
10
+
7
11
  **`slack()` from `@trigger.dev/slack` turns a `chat.agent` into a native Slack agent.** Each Slack agent session (a conversation with the agent, or a thread in a channel) becomes a durable Trigger.dev [session](/ai-chat/sessions). A message runs as a turn, the session shows the agent working, the reply streams into the thread as the model writes it, and Slack's stop button stops the turn.
8
12
 
9
13
  ```ts /trigger/support.ts
@@ -209,11 +213,15 @@ export const support = chat.agent({
209
213
  });
210
214
  ```
211
215
 
212
- `onAction` also gets the click itself as `channel.event`, typed with `SlackBlockActions`: who clicked, and the message the button was on. An action doesn't have to answer; return nothing to only do work:
216
+ `onAction` also gets the click itself as `channel.event`, typed with `SlackBlockActions`: who clicked, and the message the button was on. An action doesn't have to answer; return nothing to only do work. Here `actionSchema` accepts a `pick` action alongside `ask`:
213
217
 
214
218
  ```ts
215
219
  import type { SlackBlockActions } from "@trigger.dev/slack";
216
220
 
221
+ actionSchema: z.discriminatedUnion("type", [
222
+ z.object({ type: z.literal("ask"), question: z.string() }),
223
+ z.object({ type: z.literal("pick"), name: z.string() }),
224
+ ]),
217
225
  onAction: async ({ action, channel }) => {
218
226
  const click = channel?.event as SlackBlockActions | undefined;
219
227
  if (action.type === "pick") {
@@ -244,7 +252,7 @@ import { slack, mentions } from "@trigger.dev/slack";
244
252
  slack({ id: "slack", endpoint: slackEvents, token, startOn: mentions("U012BOT") });
245
253
  ```
246
254
 
247
- When someone mentions the agent partway through a thread, `seedFromThread: true` loads the thread's earlier replies into the new session first, so the agent answers with the context a reader of the thread has. It reads `conversations.replies` once, on the session's first turn, and keeps the newest 50 replies (`{ limit }` changes that). New non-Marketplace apps are limited by Slack to 15 replies per call and one call a minute; a rate limit keeps what was fetched rather than failing the turn.
255
+ When someone mentions the agent partway through a thread, `seedFromThread: true` loads the thread's earlier replies into the new session first, so the agent answers with the context a reader of the thread has. It loads the history once, on the session's first turn, paging through `conversations.replies` and keeping the newest 50 replies (`{ limit }` changes that). Slack limits new commercially distributed apps outside the Marketplace to 15 replies per call and one call a minute, so such an app may only get the thread's first page; internal apps keep the normal limits. A rate limit keeps what was fetched rather than failing the turn.
248
256
 
249
257
  ## Messages sent while the agent is working
250
258
 
@@ -349,8 +357,8 @@ slack({
349
357
  | `delivery` | `"stream"` or `"final"` | `"stream"` (default) streams the reply natively. `"final"` posts it once when the turn ends. |
350
358
  | `title` | `(prompt, event) => string \| null` | The session title, set on its first turn. Defaults to the first line of the prompt. |
351
359
  | `progress` | `"tasks"` or `"off"` | Tool calls as task cards in the streamed reply. Default `"tasks"`. |
352
- | `startOn` | `string` | Only start a session in a channel when the event matches (see `mentions`). Existing sessions always resume, and the agent's own conversation always starts one. |
353
- | `filter` | `string` | Extra [filter](/webhooks/filters) for message events, composed with the built-in guard that drops the bot's own messages. |
360
+ | `startOn` | filter string | Only start a session in a channel when the event matches (see `mentions`). Existing sessions always resume, and the agent's own conversation always starts one. Checked against the endpoint's event type. |
361
+ | `filter` | filter string | Extra [filter](/webhooks/filters) for message events, composed with the built-in guard that drops the bot's own messages. Checked against the endpoint's event type, like `webhook()`'s `filter`. |
354
362
  | `key` | `string` | Session [key](/webhooks/session-routing) template. Defaults to one session per thread. |
355
363
  | `seedFromThread` | `boolean` or `{ limit }` | Load a thread's earlier replies into a new session. Off by default. |
356
364
  | `approval` | `(call, { button }) => message` | The UI for a tool with no `execute`. Defaults to Approve and Deny resolving to `{ approved }`. |
@@ -4,6 +4,10 @@ description: "Choose an endpoint's source, configure verification, and type the
4
4
  sidebarTitle: "Sources & verification"
5
5
  ---
6
6
 
7
+ import WebhooksBetaBanner from "/snippets/webhooks-beta-banner.mdx";
8
+
9
+ <WebhooksBetaBanner />
10
+
7
11
  An endpoint's `source` defines how Trigger.dev verifies requests and types the event every subscriber receives. Choose a named provider source or supply a verification configuration with `webhooks.custom()`.
8
12
 
9
13
  ## Presets
@@ -4,6 +4,10 @@ description: "Pause a run until a matching webhook arrives on an endpoint, then
4
4
  sidebarTitle: "Waiters"
5
5
  ---
6
6
 
7
+ import WebhooksBetaBanner from "/snippets/webhooks-beta-banner.mdx";
8
+
9
+ <WebhooksBetaBanner />
10
+
7
11
  **A waiter pauses a run until a matching delivery arrives on an [endpoint](/webhooks/overview).** Use it when your task starts something in another system and needs to know when that system reports back: a payment that settles, a build that finishes, a document that gets signed.
8
12
 
9
13
  ```ts /trigger/checkout.ts
@@ -12,7 +16,15 @@ import { stripe } from "./stripe"; // webhooks.endpoint.define({ id: "stripe", s
12
16
 
13
17
  export const checkout = task({
14
18
  id: "checkout",
15
- run: async ({ orderId }: { orderId: string }) => {
19
+ run: async ({
20
+ orderId,
21
+ customerId,
22
+ paymentMethodId,
23
+ }: {
24
+ orderId: string;
25
+ customerId: string;
26
+ paymentMethodId: string;
27
+ }) => {
16
28
  const waiter = await webhooks.waiter.create(stripe, {
17
29
  match: {
18
30
  "event.type": "payment_intent.succeeded",
@@ -23,7 +35,19 @@ export const checkout = task({
23
35
  idempotencyKey: `payment-${orderId}`,
24
36
  });
25
37
 
26
- await stripeClient.paymentIntents.create({ amount: 4200, currency: "usd", metadata: { orderId } });
38
+ // confirm: true charges the customer's saved payment method now, so Stripe sends payment_intent.succeeded.
39
+ // Stripe's own idempotency key stops a retry from creating a second intent for the order.
40
+ await stripeClient.paymentIntents.create(
41
+ {
42
+ amount: 4200,
43
+ currency: "usd",
44
+ customer: customerId,
45
+ payment_method: paymentMethodId,
46
+ confirm: true,
47
+ metadata: { orderId },
48
+ },
49
+ { idempotencyKey: `payment-intent-${orderId}` }
50
+ );
27
51
 
28
52
  const { event, deliveryId } = await webhooks.waiter.wait(waiter).unwrap();
29
53
  return { paymentIntent: event.data.object.id, deliveryId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trigger.dev/sdk",
3
- "version": "4.8.0-webhooks.20261015065339",
3
+ "version": "4.8.0-webhooks.20261020092920",
4
4
  "description": "trigger.dev Node.JS SDK",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -69,12 +69,13 @@
69
69
  "dependencies": {
70
70
  "@opentelemetry/api": "1.9.1",
71
71
  "@opentelemetry/semantic-conventions": "1.41.1",
72
- "@trigger.dev/core": "4.8.0-webhooks.20261015065339",
72
+ "@trigger.dev/core": "4.8.0-webhooks.20261020092920",
73
73
  "uncrypto": "^0.1.3"
74
74
  },
75
75
  "devDependencies": {
76
76
  "@ai-sdk/provider": "3.0.8",
77
77
  "@arethetypeswrong/cli": "^0.18.5",
78
+ "@opentelemetry/sdk-trace-base": "2.8.0",
78
79
  "@types/react": "^19.2.14",
79
80
  "ai": "^6.0.116",
80
81
  "ai-v7": "npm:ai@7.0.0-canary.159",