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

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 +69 -2
  2. package/dist/commonjs/v3/ai.js +120 -30
  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 +19 -5
  8. package/dist/commonjs/v3/webhooks.js +19 -2
  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 +69 -2
  12. package/dist/esm/v3/ai.js +119 -29
  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 +19 -5
  18. package/dist/esm/v3/webhooks.js +19 -2
  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 +12 -3
  22. package/docs/ai-chat/client-protocol.mdx +6 -3
  23. package/docs/ai-chat/patterns/redacting-telemetry.mdx +117 -0
  24. package/docs/ai-chat/reference.mdx +3 -0
  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 +23 -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 +19 -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 +31 -5
  43. package/docs/webhooks/connect.mdx +14 -3
  44. package/docs/webhooks/deliveries.mdx +8 -1
  45. package/docs/webhooks/filters.mdx +39 -2
  46. package/docs/webhooks/human-in-the-loop.mdx +11 -2
  47. package/docs/webhooks/linear.mdx +5 -4
  48. package/docs/webhooks/overview.mdx +16 -0
  49. package/docs/webhooks/session-routing.mdx +11 -2
  50. package/docs/webhooks/slack.mdx +51 -18
  51. package/docs/webhooks/sources.mdx +36 -3
  52. package/docs/webhooks/waiters.mdx +22 -2
  53. package/package.json +3 -2
@@ -60,7 +60,7 @@ The id is free-form and opaque to Trigger.dev — a commit SHA, a release tag, a
60
60
  <Step title="Deploy your tasks with an external id">
61
61
 
62
62
  ```bash
63
- npx trigger.dev@latest deploy --external-id "$(git rev-parse HEAD)"
63
+ npx trigger.dev@latest deploy --env prod --external-id "$(git rev-parse HEAD)"
64
64
  ```
65
65
 
66
66
  </Step>
@@ -240,7 +240,7 @@ Read the commit in the place where it *is* available, and write it into both hal
240
240
 
241
241
  ```bash
242
242
  # 1. Deploy side — name the deployment after the commit.
243
- npx trigger.dev@latest deploy --external-id "$(git rev-parse HEAD)"
243
+ npx trigger.dev@latest deploy --env prod --external-id "$(git rev-parse HEAD)"
244
244
 
245
245
  # 2. Application side — give the running app the same value.
246
246
  TRIGGER_EXTERNAL_DEPLOYMENT_ID=<the-same-commit-sha>
@@ -385,7 +385,7 @@ If you filter deploys by path, derive the id from the same filter on both sides
385
385
 
386
386
  ```bash
387
387
  TASK_ID=$(git log -1 --format=%H -- trigger/)
388
- npx trigger.dev@latest deploy --external-id "$TASK_ID"
388
+ npx trigger.dev@latest deploy --env prod --external-id "$TASK_ID"
389
389
  ```
390
390
 
391
391
  Compute the same value where you set your app's variable. In GitHub Actions this needs `fetch-depth: 0` on `actions/checkout` — the default shallow clone can't see far enough back.
@@ -162,7 +162,7 @@ To keep your app and tasks in sync without coordinating promotion at all, see [a
162
162
 
163
163
  ## Staging deploys
164
164
 
165
- By default, the `deploy` command will deploy to the `prod` environment. If you want to deploy to a different environment, you can use the `--env` flag:
165
+ Use the `--env` flag to choose the environment you deploy to. If you don't pass it, the `deploy` command deploys to the `prod` environment, unless `TRIGGER_ACCESS_TOKEN` holds an API key for another environment. To deploy to staging:
166
166
 
167
167
  ```bash
168
168
  npx trigger.dev deploy --env staging
@@ -147,7 +147,7 @@ Creating and deploying a preview branch manually is easy:
147
147
  npx trigger.dev@latest deploy --env preview
148
148
  ```
149
149
 
150
- This will create and deploy a preview branch, automatically detecting the git branch. If for some reason the auto-detection doesn't work it will let you know and tell you do this:
150
+ This will create and deploy a preview branch, automatically detecting the git branch. When your CI checks out a detached HEAD, the branch is read from the CI provider's branch variable instead (GitLab, Bitbucket, CircleCI, Buildkite, Travis CI, Jenkins and Azure Pipelines are supported). If for some reason the auto-detection doesn't work it will let you know and tell you do this:
151
151
 
152
152
  ```bash
153
153
  npx trigger.dev@latest deploy --env preview --branch your-branch-name
@@ -47,7 +47,7 @@ jobs:
47
47
  env:
48
48
  TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
49
49
  run: |
50
- npx trigger.dev@latest deploy --external-id ${{ github.sha }}
50
+ npx trigger.dev@latest deploy --env prod --external-id ${{ github.sha }}
51
51
  ```
52
52
 
53
53
  ```yaml .github/workflows/release-trigger-staging.yml
@@ -184,7 +184,7 @@ Tip: add the `trigger.dev` CLI to your `devDependencies` and the deploy command
184
184
  ```json
185
185
  {
186
186
  "scripts": {
187
- "deploy:trigger-prod": "trigger deploy",
187
+ "deploy:trigger-prod": "trigger deploy --env prod",
188
188
  "deploy:trigger": "trigger deploy --env staging"
189
189
  },
190
190
  "devDependencies": {
@@ -254,7 +254,7 @@ jobs:
254
254
  TRIGGER_ACCESS_TOKEN: ${{ secrets.TRIGGER_ACCESS_TOKEN }}
255
255
  TRIGGER_API_URL: ${{ secrets.TRIGGER_API_URL }}
256
256
  run: |
257
- npx trigger.dev@latest deploy
257
+ npx trigger.dev@latest deploy --env prod
258
258
  ```
259
259
 
260
260
  </CodeGroup>
package/docs/limits.mdx CHANGED
@@ -12,6 +12,12 @@ You can view your current limits, quotas, and rate limit usage in real-time by v
12
12
  [Billing limits and alerts](/billing-limits).
13
13
  </Note>
14
14
 
15
+ ## Run duration
16
+
17
+ 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.
18
+
19
+ Trigger is built for long running AI agents and workflows and we have seen Trigger.dev users with runs lasting more than six months.
20
+
15
21
  ## Concurrency limits
16
22
 
17
23
  Concurrency is allocated **per environment**, not as a single figure for the whole organization. Each plan sets a separate default for production, staging, preview and development:
@@ -109,7 +109,7 @@ Then add scripts to your `package.json`:
109
109
  {
110
110
  "scripts": {
111
111
  "dev:trigger": "trigger dev",
112
- "deploy:trigger": "trigger deploy"
112
+ "deploy:trigger": "trigger deploy --env prod"
113
113
  }
114
114
  }
115
115
  ```
@@ -189,7 +189,7 @@ Execute a TRQL query against your Trigger.dev data. TRQL is a SQL-style query la
189
189
 
190
190
  ### list_dashboards
191
191
 
192
- List available built-in dashboards with their widgets. Each dashboard contains pre-built queries for common metrics.
192
+ List the built-in dashboards and your organization's custom dashboards, with their widgets.
193
193
 
194
194
  **Example usage:**
195
195
  - `"What dashboards are available?"`
@@ -197,12 +197,28 @@ List available built-in dashboards with their widgets. Each dashboard contains p
197
197
 
198
198
  ### run_dashboard_query
199
199
 
200
- Execute a single widget query from a built-in dashboard. Use `list_dashboards` first to see available dashboards and widget IDs.
200
+ Execute a single widget query from a built-in or custom dashboard. Use `list_dashboards` first to see available dashboards and widget IDs.
201
201
 
202
202
  **Example usage:**
203
203
  - `"Run the total runs widget from the overview dashboard"`
204
204
  - `"Show me the LLM cost over time from the AI dashboard"`
205
205
 
206
+ ### create_dashboard
207
+
208
+ Create a custom dashboard, optionally with widgets. Each widget has a title, a TRQL query and a display: a table, a bar or line chart, a big number, or a section title. Queries are checked when the dashboard is saved.
209
+
210
+ **Example usage:**
211
+ - `"Make a dashboard for the email tasks with total runs, failures and runs per day"`
212
+ - `"Create a dashboard that charts p95 duration by task"`
213
+
214
+ ### set_dashboard_widgets
215
+
216
+ Replace every widget on a custom dashboard, and optionally rename it.
217
+
218
+ **Example usage:**
219
+ - `"Add a failed runs widget to the email dashboard"`
220
+ - `"Change the runs per day chart to a line chart"`
221
+
206
222
  ### get_report
207
223
 
208
224
  Render an interpreted [health report](/reports) — a deterministic verdict, not a raw panel — as text with sparklines. The `health` report answers whether work is flowing, whether the runs that start are healthy, and whether the telemetry is fresh, with a headline verdict and a suggested next action. Returns markdown by default, or ANSI when `color` is set. Read-only.
@@ -239,7 +255,7 @@ Check the status of the dev server and view recent output. Shows whether it is s
239
255
  - `"Are there any build errors?"`
240
256
 
241
257
  <Callout type="warning">
242
- The deploy and list_preview_branches tools are not available when the MCP server is running with the `--dev-only` flag. The `--readonly` flag hides deploy, trigger_task, cancel_run, and replay_run.
258
+ The deploy and list_preview_branches tools are not available when the MCP server is running with the `--dev-only` flag. The `--readonly` flag hides deploy, trigger_task, cancel_run, replay_run, create_dashboard, and set_dashboard_widgets.
243
259
  </Callout>
244
260
 
245
261
  ## Agent Chat Tools
@@ -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. |
@@ -11,14 +11,21 @@ List channels on a [`chat.agent`](/ai-chat/overview) alongside (or instead of) `
11
11
  ```ts
12
12
  import { webhooks } from "@trigger.dev/sdk";
13
13
  import { chat } from "@trigger.dev/sdk/ai";
14
- import { slack, webhookSource } from "@trigger.dev/slack";
14
+ import { appMentions, slack, webhookSource } from "@trigger.dev/slack";
15
15
  import { anthropic } from "@ai-sdk/anthropic";
16
16
 
17
17
  const slackEvents = webhooks.endpoint.define({ id: "slack", source: webhookSource() });
18
18
 
19
19
  export const supportAgent = chat.agent({
20
20
  id: "support-agent",
21
- channels: [slack({ id: "support-slack", endpoint: slackEvents, token: process.env.SLACK_BOT_TOKEN! })],
21
+ channels: [
22
+ slack({
23
+ id: "support-slack",
24
+ endpoint: slackEvents,
25
+ token: process.env.SLACK_BOT_TOKEN!,
26
+ startOn: appMentions(),
27
+ }),
28
+ ],
22
29
  run: async ({ messages, streamText }) =>
23
30
  streamText({ model: anthropic("claude-sonnet-4-5"), messages }),
24
31
  });
@@ -59,17 +66,36 @@ const mySurface = chat.channels.custom({
59
66
  outbound: (reply) => (reply.text ? { text: reply.text } : null), // null posts nothing
60
67
  send: async (message, ctx) => {
61
68
  const ref = await postToMySurface(ctx.event, message.text, ctx.previousRef);
62
- return { ref }; // an existing ref means edit-in-place on the next turn
69
+ return { ref }; // later sends in the same turn get it back as ctx.previousRef
63
70
  },
64
71
  });
65
72
  ```
66
73
 
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).
74
+ `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
75
 
69
76
  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
77
 
71
78
  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
79
 
80
+ ## When a turn fails
81
+
82
+ 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.
83
+
84
+ `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.
85
+
86
+ ```ts
87
+ slack({
88
+ id: "slack",
89
+ endpoint: slackEvents,
90
+ token: process.env.SLACK_BOT_TOKEN!,
91
+ startOn: appMentions(),
92
+ errorMessage: ({ error, runId }) =>
93
+ error.message.includes("rate limit")
94
+ ? "I'm getting a lot of requests right now. Try again in a minute."
95
+ : `Something went wrong answering this. Try again, or share ref ${runId} with support.`,
96
+ });
97
+ ```
98
+
73
99
  ## Stopping a turn from the channel
74
100
 
75
101
  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:
@@ -88,7 +114,7 @@ When nothing is running, the event is handled by `inbound` as an ordinary messag
88
114
  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
115
 
90
116
  - `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.
117
+ - `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.
92
118
 
93
119
  ## Bound sessions
94
120
 
@@ -18,8 +18,8 @@ The prompt tells the agent to use the Trigger.dev MCP server (`npx trigger.dev@l
18
18
  | --- | --- |
19
19
  | `list_webhook_endpoints` | Lists the environment's endpoints, with their URL and whether a secret is set. |
20
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. |
21
+ | `generate_webhook_secret` | Generates a signing secret and returns it once, for providers where you choose the secret. Optionally keeps accepting the old secret (`previousSecretTtlSeconds`). |
22
+ | `set_webhook_secret` | Stores a secret the provider issued. Optionally keeps accepting the old secret (`previousSecretTtlSeconds`). |
23
23
 
24
24
  Without the MCP server, the same operations are available on the REST API with the environment's secret key:
25
25
 
@@ -74,7 +74,18 @@ The verify token answers the URL check. The signing secret verifies event delive
74
74
 
75
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
76
 
77
+ Both dialogs ask how long to keep accepting the old secret: **Stop immediately**, 1 hour, 24 hours (the default) or 7 days. Until then, deliveries signed with either secret verify, so you can update the provider without failing any. Choose **Stop immediately** if the old secret leaked.
78
+
79
+ Through the MCP tools or the REST API, pass `previousSecretTtlSeconds` (at most 604800, 7 days). The response's `previousSecretExpiresAt` says when the old secret stops working. Without it, the old secret stops working straight away:
80
+
81
+ ```bash
82
+ curl -X POST "https://api.trigger.dev/api/v1/webhooks/endpoints/github/rotate-secret" \
83
+ -H "Authorization: Bearer $TRIGGER_SECRET_KEY" \
84
+ -H "Content-Type: application/json" \
85
+ -d '{"previousSecretTtlSeconds": 3600}'
86
+ ```
87
+
77
88
  <Warning>
78
89
  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.
90
+ before closing the dialog.
80
91
  </Warning>
@@ -81,7 +81,14 @@ Trigger.dev uses the endpoint and idempotency key to deduplicate incoming events
81
81
 
82
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
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.
84
+ 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:
85
+
86
+ | Key | Sources |
87
+ | --- | --- |
88
+ | 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`), Hookdeck (`X-Hookdeck-Eventid`, or `<prefix>-eventid` with a custom header prefix) |
89
+ | A body field | Stripe, Notion, Vercel, Intercom, WorkOS, Metronome, Mux, Langfuse and Plain (`id`), Square, Slack and Typeform (`event_id`), Firecrawl (`webhookId`) |
90
+
91
+ The other named sources (Zoom, Calendly, Sentry, DocuSign, ElevenLabs, Vapi, Retell, Tailscale, Infisical, PlanetScale, Supabase, Discord) use the fallback below.
85
92
 
86
93
  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
94
 
@@ -27,7 +27,36 @@ export const largePayments = webhook({
27
27
 
28
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
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.
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. 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.
31
+
32
+ ## Narrow a union event
33
+
34
+ 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:
35
+
36
+ ```ts /trigger/checkouts.ts
37
+ import { webhook, webhooks } from "@trigger.dev/sdk";
38
+ import type Stripe from "stripe";
39
+
40
+ export const stripe = webhooks.endpoint.define({
41
+ id: "stripe",
42
+ source: webhooks.stripe<Stripe.Event>(),
43
+ });
44
+
45
+ export const checkouts = webhook({
46
+ id: "checkouts",
47
+ endpoint: stripe,
48
+ filter: "event.type == 'checkout.session.completed' && event.data.object.amount_total >= 100",
49
+ onEvent: async ({ event }) => {
50
+ const session: Stripe.Checkout.Session = event.data.object; // no cast needed
51
+ console.log("Checkout completed", session.id);
52
+ },
53
+ });
54
+ ```
55
+
56
+ - `in [...]` selects the members for any listed value, and `||` gives the union of its branches. Parentheses group branches as usual.
57
+ - `!=`, `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.
58
+ - Without a type clause, a field that only some members have is accepted: an event without it never matches.
59
+ - 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
60
 
32
61
  ## Check the result
33
62
 
@@ -65,6 +94,8 @@ Paths must have non-empty segments. `event.data.amount` is valid; `event.`, `eve
65
94
 
66
95
  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
96
 
97
+ 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`.
98
+
68
99
  ### Comparing two fields
69
100
 
70
101
  Equality and comparison operators also accept a field path on the right-hand side:
@@ -81,9 +112,15 @@ event.billing.country == event.shipping.country
81
112
  event.items any ( quantity > 0 )
82
113
  ```
83
114
 
115
+ When the payload itself is an array, as with providers that batch several events into one request (Tailscale, for example), quantify over `event`:
116
+
117
+ ```text Filter expression
118
+ event any ( type == 'nodeCreated' )
119
+ ```
120
+
84
121
  ### Spacing
85
122
 
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.
123
+ 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
124
 
88
125
  ## Filter by event type
89
126
 
@@ -37,7 +37,7 @@ Out of the box, `slack()` posts Approve and Deny buttons for any pending no-`exe
37
37
  ```ts trigger/support-agent.ts
38
38
  import { webhooks } from "@trigger.dev/sdk";
39
39
  import { chat } from "@trigger.dev/sdk/ai";
40
- import { slack, webhookSource } from "@trigger.dev/slack";
40
+ import { appMentions, slack, webhookSource } from "@trigger.dev/slack";
41
41
  import { tool } from "ai";
42
42
  import { anthropic } from "@ai-sdk/anthropic";
43
43
  import { z } from "zod";
@@ -56,7 +56,14 @@ const requestApproval = tool({
56
56
 
57
57
  export const supportAgent = chat.agent({
58
58
  id: "support-agent",
59
- channels: [slack({ id: "support-slack", endpoint: slackEvents, token: process.env.SLACK_BOT_TOKEN! })],
59
+ channels: [
60
+ slack({
61
+ id: "support-slack",
62
+ endpoint: slackEvents,
63
+ token: process.env.SLACK_BOT_TOKEN!,
64
+ startOn: appMentions(),
65
+ }),
66
+ ],
60
67
  tools: { requestApproval },
61
68
  run: async ({ messages, tools, streamText }) =>
62
69
  streamText({
@@ -115,7 +122,9 @@ chat.channels.custom({
115
122
  text: `Approve: ${pending[0].input.action}?`,
116
123
  buttons: pending.map((c) => [`${c.toolCallId}:yes`, `${c.toolCallId}:no`]),
117
124
  }),
125
+ // called for every delivery: return null for anything that isn't one of these button clicks
118
126
  onInteraction: (e) => {
127
+ if (typeof e.buttonValue !== "string" || !e.buttonValue.includes(":")) return null;
119
128
  const [toolCallId, choice] = e.buttonValue.split(":");
120
129
  return { toolCallId, output: { approved: choice === "yes" } };
121
130
  },
@@ -44,7 +44,7 @@ The `run()` loop is the same one a browser chat or a Slack thread uses. Verifica
44
44
 
45
45
  ## What the agent sees
46
46
 
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.
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 as transcript messages, so the agent can answer with the context a reader of the issue has.
48
48
 
49
49
  A follow-up prompt on the session is just the message. If the user hits **Stop** in Linear, see [Stopping](#stopping).
50
50
 
@@ -63,7 +63,7 @@ linear({ id: "linear", endpoint: linearEvents, token, context: { guidance: false
63
63
  | Turn starts | An ephemeral `thought` ("Looking into this...") |
64
64
  | A tool is called | An ephemeral `action` with the tool name and input, then the same action with its `result` |
65
65
  | The reply is ready | A `response` with the reply as markdown |
66
- | The turn fails | An `error` |
66
+ | The turn fails | An `error` saying "Something went wrong answering this. Try again." (`errorMessage` changes it) |
67
67
  | 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)) |
68
68
  | The user picks an option | A `thought` recording the decision ("Approved by Jane.", "Jane chose "High".") |
69
69
  | 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)) |
@@ -164,14 +164,15 @@ The first Linear turn binds the session to the agent session ([bound sessions](/
164
164
  | `endpoint` | endpoint | The Linear endpoint, declared with `webhooks.endpoint.define({ id, source: webhookSource() })`. Other `webhook()` tasks can subscribe to the same endpoint. |
165
165
  | `token` | `string` or resolver | The app's OAuth access token, or a function of the event (keyed on `organizationId`) for multi-workspace installs. |
166
166
  | `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. |
167
+ | `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`. |
168
+ | `startOn` | filter string | Only start a session when the event matches. Existing sessions always resume. Checked against the endpoint's event type. |
169
169
  | `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
170
  | `progress` | `"actions"` or `"off"` | Mirror tool calls as action activities (default) or post only the response. |
171
171
  | `summarize` | `boolean` | Set the session title from each response's first line. Off by default; Linear titles the session itself. |
172
172
  | `approvals` | `{ [toolName]: (input, call) => { title?, body } }` | Custom approval card body per tool. Default lays the tool input out as markdown. |
173
173
  | `interactions` | `{ [toolName]: (call) => { title?, body, options } }` | A tool's own select card; the chosen option resolves the tool to `{ selected }`. |
174
174
  | `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. |
175
+ | `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." |
175
176
  | `ack` | message, `null`, or function | The first thought. Pass `null` to post nothing before the response. |
176
177
  | `inbound` / `outbound` | functions | Map the event to the turn, and the reply to an activity. |
177
178
  | `delivery` | `"final"` or `"stream"` | `"final"` (default) posts the response when the turn ends. `"stream"` also posts ephemeral thoughts as the reply streams. |
@@ -59,6 +59,22 @@ Each subscriber has its own [filter](/webhooks/filters), so one delivery can rea
59
59
  | `endpoint` | The endpoint the delivery arrived on: `id`, `declaredId`, `tenantId`, `externalRef`, and the `metadata` you declared. |
60
60
  | `ctx` | The same [run context](/context) that regular tasks receive. |
61
61
 
62
+ `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:
63
+
64
+ ```ts
65
+ export const orders = webhook({
66
+ id: "stripe-orders",
67
+ endpoint: stripe,
68
+ queue: { concurrencyLimit: 1 },
69
+ retry: { maxAttempts: 5 },
70
+ onEvent: async ({ event }) => {
71
+ // ...
72
+ },
73
+ });
74
+ ```
75
+
76
+ 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.
77
+
62
78
  `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.
63
79
 
64
80
  ## How it works
@@ -23,6 +23,8 @@ export const orderEvents = chat.event({
23
23
  endpoint: stripe,
24
24
  // one session per customer
25
25
  key: "{event.data.object.customer}",
26
+ // invoices always carry a customer; events without one would have no session to go to
27
+ filter: "event.type startsWith 'invoice.'",
26
28
  type: "order.event",
27
29
  });
28
30
  ```
@@ -81,7 +83,14 @@ When the same session is also a [channel](/webhooks/channels) conversation (a Sl
81
83
  ```ts
82
84
  export const orderAgent = chat.agent({
83
85
  id: "order-agent",
84
- channels: [slack({ id: "support-slack", endpoint: slackEvents, token: process.env.SLACK_BOT_TOKEN! })],
86
+ channels: [
87
+ slack({
88
+ id: "support-slack",
89
+ endpoint: slackEvents,
90
+ token: process.env.SLACK_BOT_TOKEN!,
91
+ startOn: appMentions(),
92
+ }),
93
+ ],
85
94
  events: [orderEvents],
86
95
  onAction: async ({ action }) => {
87
96
  if (action.type === "order.event") {
@@ -113,7 +122,7 @@ A bound turn posts the final answer as a fresh message in the thread (no placeho
113
122
 
114
123
  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
124
 
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.
125
+ 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
126
 
118
127
  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
128