@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.
- package/dist/commonjs/v3/ai.d.ts +69 -2
- package/dist/commonjs/v3/ai.js +120 -30
- package/dist/commonjs/v3/ai.js.map +1 -1
- package/dist/commonjs/v3/query.d.ts +434 -1
- package/dist/commonjs/v3/query.js +115 -0
- package/dist/commonjs/v3/query.js.map +1 -1
- package/dist/commonjs/v3/webhooks.d.ts +19 -5
- package/dist/commonjs/v3/webhooks.js +19 -2
- package/dist/commonjs/v3/webhooks.js.map +1 -1
- package/dist/commonjs/version.js +1 -1
- package/dist/esm/v3/ai.d.ts +69 -2
- package/dist/esm/v3/ai.js +119 -29
- package/dist/esm/v3/ai.js.map +1 -1
- package/dist/esm/v3/query.d.ts +434 -1
- package/dist/esm/v3/query.js +115 -0
- package/dist/esm/v3/query.js.map +1 -1
- package/dist/esm/v3/webhooks.d.ts +19 -5
- package/dist/esm/v3/webhooks.js +19 -2
- package/dist/esm/v3/webhooks.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/docs/ai-chat/backend.mdx +12 -3
- package/docs/ai-chat/client-protocol.mdx +6 -3
- package/docs/ai-chat/patterns/redacting-telemetry.mdx +117 -0
- package/docs/ai-chat/reference.mdx +3 -0
- package/docs/ai-chat/transcript-storage.mdx +1 -1
- package/docs/apikeys.mdx +6 -4
- package/docs/cli-runs-commands.mdx +2 -2
- package/docs/cli-tasks-commands.mdx +95 -0
- package/docs/config/config-file.mdx +58 -0
- package/docs/config/extensions/prismaExtension.mdx +4 -4
- package/docs/deploy-environment-variables.mdx +23 -0
- package/docs/deployment/atomic-deployment.mdx +3 -3
- package/docs/deployment/overview.mdx +1 -1
- package/docs/deployment/preview-branches.mdx +1 -1
- package/docs/github-actions.mdx +3 -3
- package/docs/limits.mdx +6 -0
- package/docs/manual-setup.mdx +1 -1
- package/docs/mcp-tools.mdx +19 -3
- package/docs/observability/dashboards.mdx +62 -0
- package/docs/runs/max-duration.mdx +4 -0
- package/docs/self-hosting/env/supervisor.mdx +1 -0
- package/docs/webhooks/channels.mdx +31 -5
- package/docs/webhooks/connect.mdx +14 -3
- package/docs/webhooks/deliveries.mdx +8 -1
- package/docs/webhooks/filters.mdx +39 -2
- package/docs/webhooks/human-in-the-loop.mdx +11 -2
- package/docs/webhooks/linear.mdx +5 -4
- package/docs/webhooks/overview.mdx +16 -0
- package/docs/webhooks/session-routing.mdx +11 -2
- package/docs/webhooks/slack.mdx +51 -18
- package/docs/webhooks/sources.mdx +36 -3
- package/docs/webhooks/waiters.mdx +22 -2
- 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
|
-
|
|
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
|
package/docs/github-actions.mdx
CHANGED
|
@@ -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:
|
package/docs/manual-setup.mdx
CHANGED
package/docs/mcp-tools.mdx
CHANGED
|
@@ -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
|
|
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
|
|
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: [
|
|
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 }; //
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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: [
|
|
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
|
},
|
package/docs/webhooks/linear.mdx
CHANGED
|
@@ -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
|
|
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` |
|
|
168
|
-
| `startOn` |
|
|
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: [
|
|
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
|
|
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
|
|