You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e392c23
Browse filesBrowse the repository at this point in the historyBrowse files
docs(webhooks): retry dedupe per source, null presence checks, webhook() task options, failed-turn line
Webhooks docs: which sources deduplicate provider retries, `null`
presence checks in filters, task options on `webhook()`, the message
channels show when an agent turn fails, the now-required `startOn` for
Slack agents, and a changelog for the webhooks beta builds.
Mono-RevId: 96d44314c377372ee1d26c7a101747498c395295
@@ -74,6 +81,25 @@ An event that `inbound` maps to an empty message (no text, and no file or data p
74
81
75
82
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.
76
83
84
+
## When a turn fails
85
+
86
+
When a turn fails (the model or provider returns an error, or the turn throws), the channel shows a short line instead of the error: "Something went wrong answering this. Try again." by default, or the connector's `errorMessage`. If the reply had partly streamed, the line goes after that text. The error is logged on the run, and `outbound` receives it as `reply.error`, so you can render a failure differently.
87
+
88
+
`errorMessage` is a fixed string, or a function that builds the line from the failure. It receives the `error`, the `partialText` streamed before it failed, the triggering `event` (undefined on a turn the channel didn't start), the `runId`, the `chatId`, and the `defaultMessage`. Return nothing, or throw, to fall back to the default line.
89
+
90
+
```ts
91
+
slack({
92
+
id: "slack",
93
+
endpoint: slackEvents,
94
+
token: process.env.SLACK_BOT_TOKEN!,
95
+
startOn: appMentions(),
96
+
errorMessage: ({ error, runId }) =>
97
+
error.message.includes("rate limit")
98
+
?"I'm getting a lot of requests right now. Try again in a minute."
99
+
:`Something went wrong answering this. Try again, or share ref ${runId} with support.`,
100
+
});
101
+
```
102
+
77
103
## Stopping a turn from the channel
78
104
79
105
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:
@@ -92,7 +118,7 @@ When nothing is running, the event is handled by `inbound` as an ordinary messag
92
118
Two more hooks let a connector mirror the session into a surface that has its own notion of an agent, the way `slack()` does:
93
119
94
120
-`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.
95
-
-`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.
121
+
-`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, with the connector's error line as `errorText` when the turn failed. 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.
Copy file name to clipboardExpand all lines: docs/webhooks/deliveries.mdx
+8-1Lines changed: 8 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -85,7 +85,14 @@ Trigger.dev uses the endpoint and idempotency key to deduplicate incoming events
85
85
86
86
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.
87
87
88
-
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.
88
+
When the source maps a provider event ID, that ID becomes the key, so the provider's own retries of an event are recognised as duplicates:
89
+
90
+
| Key | Sources |
91
+
| --- | --- |
92
+
| A header | GitHub (`X-GitHub-Delivery`), Svix and Autumn (`svix-id`), Standard Webhooks, Loops and Granola (`webhook-id`), Linear (`Linear-Delivery`), PagerDuty (`X-Webhook-Id`), Jira (`X-Atlassian-Webhook-Identifier`), Zendesk (`X-Zendesk-Webhook-Invocation-Id`), Attio (`Idempotency-Key`), Shopify (`X-Shopify-Webhook-Id`) |
93
+
| A body field | Stripe, Notion, Vercel, Intercom, WorkOS, Metronome, Mux, Langfuse and Plain (`id`), Square, Slack and Typeform (`event_id`), Firecrawl (`webhookId`) |
94
+
95
+
The other named sources (Zoom, Calendly, Sentry, DocuSign, ElevenLabs, Vapi, Retell, Tailscale, Infisical, PlanetScale, Supabase, Discord) use the fallback below.
89
96
90
97
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.
Copy file name to clipboardExpand all lines: docs/webhooks/filters.mdx
+2Lines changed: 2 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -98,6 +98,8 @@ Paths must have non-empty segments. `event.data.amount` is valid; `event.`, `eve
98
98
99
99
Values are strings in single quotes (`'created'`), numbers (`10000`), booleans (`true`), or `null`. Use a list of these values with `in` and `not in`.
100
100
101
+
A `null` value checks presence. `event.issue.pull_request != null` matches events that have the field, and `== null` matches events where it is missing or `null`. It works on any field, including fields your event type marks as required, because a missing field compares equal to `null`.
102
+
101
103
### Comparing two fields
102
104
103
105
Equality and comparison operators also accept a field path on the right-hand side:
| Turn starts | An ephemeral `thought` ("Looking into this...") |
68
68
| A tool is called | An ephemeral `action` with the tool name and input, then the same action with its `result`|
69
69
| The reply is ready | A `response` with the reply as markdown |
70
-
| The turn fails | An `error`|
70
+
| The turn fails | An `error`saying "Something went wrong answering this. Try again." (`errorMessage` changes it) |
71
71
| A tool needs a human decision | An `elicitation` with Approve and Deny options, or the tool's own options from `interactions` (see [Approvals and choices](#approvals-and-choices)) |
72
72
| The user picks an option | A `thought` recording the decision ("Approved by Jane.", "Jane chose "High".") |
73
73
| A prompt arrives while the agent works | A `thought` ("Got it, I'll take that into account.") as soon as it arrives (see [Prompts sent while the agent is working](#prompts-sent-while-the-agent-is-working)) |
@@ -176,6 +176,7 @@ The first Linear turn binds the session to the agent session ([bound sessions](/
176
176
|`approvals`|`{ [toolName]: (input, call) => { title?, body } }`| Custom approval card body per tool. Default lays the tool input out as markdown. |
177
177
|`interactions`|`{ [toolName]: (call) => { title?, body, options } }`| A tool's own select card; the chosen option resolves the tool to `{ selected }`. |
178
178
|`receivedThought`|`string`, function or `null`| Thought posted as soon as a prompt arrives while the agent is working. Default "Got it, I'll take that into account.", null for none. |
179
+
|`errorMessage`|`string \| (ctx) => string`| The error activity's text when a turn fails, fixed or built from the failure ([details](/webhooks/channels#when-a-turn-fails)). Default "Something went wrong answering this. Try again." |
179
180
|`ack`| message, `null`, or function | The first thought. Pass `null` to post nothing before the response. |
180
181
|`inbound` / `outbound`| functions | Map the event to the turn, and the reply to an activity. |
181
182
|`delivery`|`"final"` or `"stream"`|`"final"` (default) posts the response when the turn ends. `"stream"` also posts ephemeral thoughts as the reply streams. |
Copy file name to clipboardExpand all lines: docs/webhooks/overview.mdx
+16Lines changed: 16 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -63,6 +63,22 @@ Each subscriber has its own [filter](/webhooks/filters), so one delivery can rea
63
63
|`endpoint`| The endpoint the delivery arrived on: `id`, `declaredId`, `tenantId`, `externalRef`, and the `metadata` you declared. |
64
64
|`ctx`| The same [run context](/context) that regular tasks receive. |
65
65
66
+
`webhook()` also takes the options a [task](/tasks/overview) does, such as `queue`, `concurrency`, `machine`, `retry`, `maxDuration` and the lifecycle hooks. For example, to handle one delivery at a time:
67
+
68
+
```ts
69
+
exportconst orders =webhook({
70
+
id: "stripe-orders",
71
+
endpoint: stripe,
72
+
queue: { concurrencyLimit: 1 },
73
+
retry: { maxAttempts: 5 },
74
+
onEvent: async ({ event }) => {
75
+
// ...
76
+
},
77
+
});
78
+
```
79
+
80
+
The limit covers every delivery the task handles. To process each customer's events in order while different customers run in parallel, route them to an agent with [session routing](/webhooks/session-routing): deliveries with the same key land in one session and are handled one after another.
81
+
66
82
`webhooks.endpoint.define` also takes a `description` and `metadata`. The metadata is passed to every subscriber as `endpoint.metadata`. The endpoint `id` can't start with `wh_`, which is reserved for the ids Trigger.dev generates.
Copy file name to clipboardExpand all lines: docs/webhooks/private-beta.mdx
+40-1Lines changed: 40 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -96,10 +96,49 @@ pnpm needs 10.17 or later for the `@trigger.dev/*` pattern; on 10.16, list the p
96
96
97
97
## Stay up to date
98
98
99
-
Beta versions look like `4.8.0-webhooks.20261015065339`, the next release plus a timestamp. Run the install commands again to move to the newest build. To stay on one build, install that exact version instead of the tag.
99
+
Beta versions look like `4.8.0-webhooks.20261020092920`, the next release plus a timestamp. Run the install commands again to move to the newest build. To stay on one build, install that exact version instead of the tag.
100
100
101
101
When webhooks become generally available they ship in a normal release, and you switch back to `@latest`. You can remove the release age exemption then.
102
102
103
+
## Changelog
104
+
105
+
### 4.8.0-webhooks.20261020092920 (October 20)
106
+
107
+
**Breaking:**`slack()` now requires `startOn`. Without it, every message in every channel the bot was in started a session. `appMentions()` answers when the bot is @mentioned, and needs no bot user id:
Add `app_mention` to the app's bot events and the `app_mentions:read` scope, then reinstall the app. See [Slack agents](/webhooks/slack).
121
+
122
+
New:
123
+
124
+
- 12 more sources: `webhooks.loops()`, `granola()`, `autumn()`, `metronome()`, `mux()`, `tailscale()`, `langfuse()`, `infisical()`, `planetscale()`, `plain()`, `firecrawl()` and `supabase()`, each with a setup prompt. Supabase database webhooks aren't signed, so `supabase()` checks a bearer token you add as a header.
125
+
- Retries from PagerDuty, Jira, Zendesk, Attio, Shopify, Notion, Vercel, Typeform, Intercom, WorkOS and Firecrawl are deduplicated on the provider's own event or delivery id, so a retried event runs once. See [Deliveries](/webhooks/deliveries).
126
+
- Filters accept `== null` and `!= null` to check whether a field is present, and `event any ( ... )` matches a payload that is an array of events (Tailscale, SendGrid, HubSpot, Auth0) when any of them matches.
127
+
-`webhook()` takes `task()` options such as `queue`, `machine` and `retry`.
128
+
- A failed turn in Slack, Linear or a custom channel posts "Something went wrong answering this. Try again." instead of the raw error. `errorMessage` changes it, as a fixed line or a function of the error, run id and event. See [When a turn fails](/webhooks/channels#when-a-turn-fails).
129
+
- Slack: tool calls show as one plan in the reply, each task titled by the tool's `title` (`progress: "tasks"` gives one card per call). `ownWorkspaceOnly: true` ignores Slack Connect guests, and `SlackRunSurface` types `chat.surface<SlackRunSurface>()`.
130
+
- Dashboard: filter an endpoint's deliveries by status, live or test mode, and delivery id (ours or the provider's).
131
+
132
+
Fixed:
133
+
134
+
- Plain and Metronome deliveries verify, with each provider's replay window.
135
+
136
+
This build also includes what's on `main` since the last one, such as span redaction with `telemetry.spanProcessors`, managing metric dashboards from the API, and `trigger tasks trigger`.
137
+
138
+
### 4.8.0-webhooks.20261015065339 (October 15)
139
+
140
+
The first beta build.
141
+
103
142
## Get started
104
143
105
144
-[Overview](/webhooks/overview): endpoints, sources, and what can subscribe.
0 commit comments