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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/commonjs/v3/ai.d.ts +560 -8
  2. package/dist/commonjs/v3/ai.js +2263 -178
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat.d.ts +23 -0
  5. package/dist/commonjs/v3/chat.js.map +1 -1
  6. package/dist/commonjs/v3/concurrencyLimits.d.ts +37 -7
  7. package/dist/commonjs/v3/concurrencyLimits.js +38 -70
  8. package/dist/commonjs/v3/concurrencyLimits.js.map +1 -1
  9. package/dist/commonjs/v3/index.d.ts +3 -1
  10. package/dist/commonjs/v3/index.js +23 -17
  11. package/dist/commonjs/v3/index.js.map +1 -1
  12. package/dist/commonjs/v3/modelFileDownload.d.ts +10 -0
  13. package/dist/commonjs/v3/modelFileDownload.js +164 -0
  14. package/dist/commonjs/v3/modelFileDownload.js.map +1 -0
  15. package/dist/commonjs/v3/runs.d.ts +22 -1
  16. package/dist/commonjs/v3/runs.js +6 -3
  17. package/dist/commonjs/v3/runs.js.map +1 -1
  18. package/dist/commonjs/v3/test/index.d.ts +2 -1
  19. package/dist/commonjs/v3/test/index.js +4 -1
  20. package/dist/commonjs/v3/test/index.js.map +1 -1
  21. package/dist/commonjs/v3/test/mock-channel-connector.d.ts +172 -0
  22. package/dist/commonjs/v3/test/mock-channel-connector.js +173 -0
  23. package/dist/commonjs/v3/test/mock-channel-connector.js.map +1 -0
  24. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +81 -1
  25. package/dist/commonjs/v3/test/mock-chat-agent.js +137 -10
  26. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  27. package/dist/commonjs/v3/transcriptStorage.d.ts +44 -1
  28. package/dist/commonjs/v3/transcriptStorage.js +24 -0
  29. package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
  30. package/dist/commonjs/v3/webhookWaiters.d.ts +80 -0
  31. package/dist/commonjs/v3/webhookWaiters.js +156 -0
  32. package/dist/commonjs/v3/webhookWaiters.js.map +1 -0
  33. package/dist/commonjs/v3/webhooks.d.ts +178 -2
  34. package/dist/commonjs/v3/webhooks.js +322 -1
  35. package/dist/commonjs/v3/webhooks.js.map +1 -1
  36. package/dist/commonjs/version.js +1 -1
  37. package/dist/esm/v3/ai.d.ts +560 -8
  38. package/dist/esm/v3/ai.js +2257 -178
  39. package/dist/esm/v3/ai.js.map +1 -1
  40. package/dist/esm/v3/chat.d.ts +23 -0
  41. package/dist/esm/v3/chat.js.map +1 -1
  42. package/dist/esm/v3/concurrencyLimits.d.ts +37 -7
  43. package/dist/esm/v3/concurrencyLimits.js +38 -70
  44. package/dist/esm/v3/concurrencyLimits.js.map +1 -1
  45. package/dist/esm/v3/index.d.ts +3 -1
  46. package/dist/esm/v3/index.js +1 -0
  47. package/dist/esm/v3/index.js.map +1 -1
  48. package/dist/esm/v3/modelFileDownload.d.ts +10 -0
  49. package/dist/esm/v3/modelFileDownload.js +161 -0
  50. package/dist/esm/v3/modelFileDownload.js.map +1 -0
  51. package/dist/esm/v3/runs.d.ts +22 -1
  52. package/dist/esm/v3/runs.js +6 -3
  53. package/dist/esm/v3/runs.js.map +1 -1
  54. package/dist/esm/v3/test/index.d.ts +2 -1
  55. package/dist/esm/v3/test/index.js +2 -1
  56. package/dist/esm/v3/test/index.js.map +1 -1
  57. package/dist/esm/v3/test/mock-channel-connector.d.ts +172 -0
  58. package/dist/esm/v3/test/mock-channel-connector.js +170 -0
  59. package/dist/esm/v3/test/mock-channel-connector.js.map +1 -0
  60. package/dist/esm/v3/test/mock-chat-agent.d.ts +81 -1
  61. package/dist/esm/v3/test/mock-chat-agent.js +137 -11
  62. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  63. package/dist/esm/v3/transcriptStorage.d.ts +44 -1
  64. package/dist/esm/v3/transcriptStorage.js +24 -0
  65. package/dist/esm/v3/transcriptStorage.js.map +1 -1
  66. package/dist/esm/v3/webhookWaiters.d.ts +80 -0
  67. package/dist/esm/v3/webhookWaiters.js +149 -0
  68. package/dist/esm/v3/webhookWaiters.js.map +1 -0
  69. package/dist/esm/v3/webhooks.d.ts +178 -2
  70. package/dist/esm/v3/webhooks.js +318 -1
  71. package/dist/esm/v3/webhooks.js.map +1 -1
  72. package/dist/esm/version.js +1 -1
  73. package/docs/ai-chat/actions.mdx +6 -1
  74. package/docs/ai-chat/backend.mdx +34 -0
  75. package/docs/ai-chat/patterns/sub-agents.mdx +33 -0
  76. package/docs/ai-chat/pending-messages.mdx +2 -0
  77. package/docs/ai-chat/reference.mdx +50 -2
  78. package/docs/apikeys.mdx +3 -3
  79. package/docs/billing-limits.mdx +1 -1
  80. package/docs/cli-env-commands.mdx +5 -4
  81. package/docs/cli-init-commands.mdx +27 -1
  82. package/docs/cli-login-commands.mdx +20 -0
  83. package/docs/cli-projects-commands.mdx +13 -1
  84. package/docs/cli-runs-commands.mdx +3 -2
  85. package/docs/cli-update-commands.mdx +9 -3
  86. package/docs/cli-whoami-commands.mdx +12 -0
  87. package/docs/concurrency.mdx +32 -0
  88. package/docs/config/extensions/audioWaveform.mdx +4 -1
  89. package/docs/config/extensions/overview.mdx +2 -0
  90. package/docs/config/extensions/prismaExtension.mdx +2 -3
  91. package/docs/config/extensions/puppeteer.mdx +3 -5
  92. package/docs/config/extensions/pythonExtension.mdx +9 -1
  93. package/docs/config/extensions/syncEnvVars.mdx +6 -0
  94. package/docs/deploy-environment-variables.mdx +1 -1
  95. package/docs/deployment/atomic-deployment.mdx +2 -0
  96. package/docs/deployment/dev-branches.mdx +5 -5
  97. package/docs/deployment/overview.mdx +8 -7
  98. package/docs/deployment/preview-branches.mdx +6 -3
  99. package/docs/errors-retrying.mdx +4 -4
  100. package/docs/how-it-works.mdx +5 -5
  101. package/docs/introduction.mdx +6 -0
  102. package/docs/limits.mdx +26 -8
  103. package/docs/logging.mdx +1 -1
  104. package/docs/machines.mdx +1 -1
  105. package/docs/mcp-introduction.mdx +8 -7
  106. package/docs/mcp-tools.mdx +91 -1
  107. package/docs/migrating-from-v3.mdx +1 -1
  108. package/docs/observability/dashboards.mdx +2 -0
  109. package/docs/observability/query.mdx +6 -2
  110. package/docs/private-networking/aws-console-setup.mdx +1 -1
  111. package/docs/private-networking/overview.mdx +1 -1
  112. package/docs/quick-start.mdx +3 -40
  113. package/docs/realtime/auth.mdx +1 -1
  114. package/docs/realtime/backend/input-streams.mdx +159 -0
  115. package/docs/realtime/backend/streams.mdx +25 -10
  116. package/docs/realtime/how-it-works.mdx +1 -1
  117. package/docs/realtime/react-hooks/overview.mdx +1 -1
  118. package/docs/realtime/react-hooks/streams.mdx +2 -2
  119. package/docs/realtime/react-hooks/subscribe.mdx +19 -3
  120. package/docs/realtime/react-hooks/triggering.mdx +2 -2
  121. package/docs/realtime/react-hooks/use-wait-token.mdx +1 -1
  122. package/docs/realtime/run-object.mdx +47 -24
  123. package/docs/replaying.mdx +26 -1
  124. package/docs/reports.mdx +4 -2
  125. package/docs/run-tests.mdx +2 -2
  126. package/docs/run-usage.mdx +3 -3
  127. package/docs/runs/heartbeats.mdx +2 -2
  128. package/docs/runs/list-best-practices.mdx +234 -0
  129. package/docs/runs/metadata.mdx +7 -10
  130. package/docs/runs.mdx +18 -8
  131. package/docs/self-hosting/env/supervisor.mdx +2 -0
  132. package/docs/self-hosting/env/webapp.mdx +5 -0
  133. package/docs/self-hosting/overview.mdx +26 -0
  134. package/docs/tags.mdx +6 -0
  135. package/docs/tasks/scheduled.mdx +11 -5
  136. package/docs/triggering.mdx +8 -0
  137. package/docs/troubleshooting-alerts.mdx +102 -3
  138. package/docs/troubleshooting.mdx +1 -1
  139. package/docs/upgrading-packages.mdx +3 -3
  140. package/docs/vercel-integration.mdx +13 -3
  141. package/docs/versioning.mdx +1 -1
  142. package/docs/wait-for-token.mdx +3 -3
  143. package/docs/webhooks/channels.mdx +128 -0
  144. package/docs/webhooks/connect.mdx +80 -0
  145. package/docs/webhooks/deliveries.mdx +92 -0
  146. package/docs/webhooks/filters.mdx +109 -0
  147. package/docs/webhooks/human-in-the-loop.mdx +149 -0
  148. package/docs/webhooks/linear.mdx +185 -0
  149. package/docs/webhooks/overview.mdx +99 -0
  150. package/docs/webhooks/session-routing.mdx +132 -0
  151. package/docs/webhooks/slack.mdx +360 -0
  152. package/docs/webhooks/sources.mdx +217 -0
  153. package/docs/webhooks/waiters.mdx +170 -0
  154. package/package.json +3 -2
@@ -0,0 +1,92 @@
1
+ ---
2
+ title: "Deliveries and endpoints"
3
+ description: "Inspect webhook payloads, where each delivery went, and the runs it started or resumed."
4
+ sidebarTitle: "Deliveries and endpoints"
5
+ ---
6
+
7
+ **A delivery records a verified webhook event and what happened to it at each subscriber.** Find deliveries on an endpoint's **Deliveries** tab. A run that a delivery triggered links back to it, and to its endpoint, from the run's **Detail** tab.
8
+
9
+ ## Endpoints
10
+
11
+ The environment's **Webhooks** page lists its endpoints with their URL, whether the signing secret is set, the last 24 hours of activity, and the last delivery. Open an endpoint for its tabs:
12
+
13
+ | Tab | Shows |
14
+ | --- | --- |
15
+ | **Deliveries** | The endpoint's deliveries, filterable by status and time. |
16
+ | **Subscribers** | Every task, agent session and channel subscribed to the endpoint, with each one's filter. |
17
+ | **Waiters** | Runs currently [waiting](/webhooks/waiters) on the endpoint. |
18
+ | **Test** | Sends a sample event through the endpoint. |
19
+
20
+ The endpoint's details show its URL, source, verification scheme and signing secret actions. See [Connect a provider](/webhooks/connect).
21
+
22
+ ### Disable an endpoint
23
+
24
+ Disable an endpoint to stop accepting its deliveries without removing it from your code. Its status becomes `inactive` and its URL responds with `404`, so the provider records failed deliveries and retries them on its own schedule:
25
+
26
+ ```bash
27
+ curl -X POST "https://api.trigger.dev/api/v1/webhooks/endpoints/stripe/disable" \
28
+ -H "Authorization: Bearer $TRIGGER_SECRET_KEY"
29
+ ```
30
+
31
+ [Enable](/management/webhooks/enable-endpoint) it again with `POST /api/v1/webhooks/endpoints/{endpointId}/enable`. An endpoint that is no longer in the environment's current deployment is also `inactive`.
32
+
33
+ ## Delivery status
34
+
35
+ | Status | Meaning |
36
+ | --- | --- |
37
+ | `PENDING` | Waiting to be routed, including after a retryable error at a subscriber. |
38
+ | `PROCESSING` | Being routed. A delivery that resumes many waiting runs stays here until they have all been resumed. |
39
+ | `SUCCEEDED` | At least one subscriber or waiting run received it, and nothing failed. |
40
+ | `FAILED` | A subscriber failed after its retries. The delivery's error names it. |
41
+ | `FILTERED` | No subscriber's filter matched, or the endpoint has no subscribers. The reason shows the failed clause. |
42
+ | `UNMATCHED` | Runs were waiting on the endpoint, but none matched and no subscriber took it. |
43
+
44
+ Delivery status tracks routing. Each run it started has its own status and retry history.
45
+
46
+ ## Where a delivery went
47
+
48
+ Open a delivery to see its payload and headers, and a row per subscriber with its result (`SUCCEEDED`, `FILTERED` with the reason, or `FAILED` with the error) and a link to the run it started. Waiting runs are summarized in one row: how many waiters matched, how many were resumed, and how many failed to resume.
49
+
50
+ A subscriber that fails transiently is retried on its own; subscribers that already succeeded are not run again.
51
+
52
+ ## Replay
53
+
54
+ Replaying a delivery routes its event again as a new delivery. Filters are checked again, so the replay reaches the subscribers that would take the event now:
55
+
56
+ ```bash
57
+ curl -X POST "https://api.trigger.dev/api/v1/webhooks/deliveries/whd_1234/replay" \
58
+ -H "Authorization: Bearer $TRIGGER_SECRET_KEY"
59
+ ```
60
+
61
+ Add `?targetId=` to send it to one subscriber regardless of its filter. `targetId` is the subscriber id: a `webhook()` task's id, or `agentId:eventId` for an agent's `chat.event` or channel. A replay never resumes waiting runs.
62
+
63
+ ## From an agent or the API
64
+
65
+ The [MCP server](/mcp-introduction) has `list_webhook_deliveries` (filter by endpoint, status and time), `get_webhook_delivery` (each subscriber's result, the waiter summary, the event and headers) and `replay_webhook_delivery`. The same data is on the REST API:
66
+
67
+ | Request | What it does |
68
+ | --- | --- |
69
+ | [`GET /api/v1/webhooks/deliveries`](/management/webhooks/list-deliveries) | Lists deliveries, newest first. Filter with `filter[endpoint]` (declared or `wh_` ids), `filter[status]`, and `filter[period]` or `filter[from]`/`filter[to]`. |
70
+ | [`GET /api/v1/webhooks/deliveries/{deliveryId}`](/management/webhooks/retrieve-delivery) | Reads a delivery, with a `targets` entry per subscriber and one for waiting runs. |
71
+ | [`POST /api/v1/webhooks/deliveries/{deliveryId}/replay`](/management/webhooks/replay-delivery) | Replays it, as above. The key needs permission to trigger every task the replay starts, and to write sessions if it reaches an agent's subscriber. |
72
+
73
+ ## Deduplication
74
+
75
+ Trigger.dev uses the endpoint and idempotency key to deduplicate incoming events. Delivery records and task runs have separate deduplication windows:
76
+
77
+ | What is deduplicated | Window |
78
+ | --- | --- |
79
+ | Delivery records | Six hours by default. |
80
+ | Task runs | Three days for Stripe; one day for other sources. |
81
+
82
+ A retry after the delivery window expires can create another delivery record. If the task-run idempotency window is still active, routing returns the existing run instead of starting another one.
83
+
84
+ When the source maps a provider event ID, that ID becomes the key. For example, Stripe uses the event body's `id`, and GitHub uses the `X-GitHub-Delivery` header.
85
+
86
+ If the source doesn't map an event ID, the key is derived from the raw body, timestamp, and signature. A retry with a different timestamp or signature can therefore produce a different key. For a [custom source](/webhooks/sources#custom-providers), set `idempotencyField` to a stable event identifier when the provider supplies one.
87
+
88
+ ## Verification and recording
89
+
90
+ Requests must pass the source's verification checks before they are recorded. Timestamp checks apply when configured by the source. A verification failure returns the source's rejected HTTP status (`400` by default) and creates no delivery or run.
91
+
92
+ Provider handshakes receive their configured response without creating a delivery. Other verified events are checked for duplicates, recorded, and routed. Filtered and unmatched deliveries stay visible, so you can see what arrived and why nothing ran.
@@ -0,0 +1,109 @@
1
+ ---
2
+ title: "Filtering deliveries"
3
+ description: "Choose which verified webhook events reach each subscriber, by payload fields, request headers, or endpoint metadata."
4
+ sidebarTitle: "Filters"
5
+ ---
6
+
7
+ **A filter selects which verified deliveries reach a subscriber.** Each subscriber on an [endpoint](/webhooks/overview) has its own filter, so one delivery can start one task and skip another. A delivery no subscriber takes is recorded with status `FILTERED`, so you can inspect it in the dashboard.
8
+
9
+ ## Add a filter
10
+
11
+ Set `filter` on the subscriber. This example accepts Stripe payment-intent events whose amount is at least 10,000:
12
+
13
+ ```ts /trigger/payment-events.ts
14
+ import { webhook, webhooks } from "@trigger.dev/sdk";
15
+
16
+ export const stripe = webhooks.endpoint.define({ id: "stripe", source: webhooks.stripe() });
17
+
18
+ export const largePayments = webhook({
19
+ id: "large-payments",
20
+ endpoint: stripe,
21
+ filter: "event.type == 'payment_intent.succeeded' && event.data.object.amount >= 10000",
22
+ onEvent: async ({ event }) => {
23
+ console.log("Matched payment event", event.id);
24
+ },
25
+ });
26
+ ```
27
+
28
+ The same `filter` works on a [`chat.event`](/webhooks/session-routing#filtering), a channel connector, and a [waiter](/webhooks/waiters), where it is checked after the waiter's `match`.
29
+
30
+ The filter is checked against the [endpoint's event type](/webhooks/sources#typing-the-event). Concrete types catch missing fields and incompatible literals. Fields typed as `unknown`, including values in Stripe's default `data.object`, allow arbitrary non-empty field paths. Operator literals and path structure are still checked.
31
+
32
+ ## Check the result
33
+
34
+ Send an event and open its [delivery](/webhooks/deliveries) in the dashboard. Each subscriber's row shows whether it took the event, and a filtered subscriber shows the failed clause and its value. The provider receives the source's accepted HTTP status (`200` by default) either way.
35
+
36
+ <Note>
37
+ If filter evaluation throws an exception, Trigger.dev routes the event. Filters aren't a substitute
38
+ for signature verification or validation inside your handler.
39
+ </Note>
40
+
41
+ ## The expression language
42
+
43
+ A filter is one or more `path operator value` clauses combined with `&&` and `||` (use parentheses to group).
44
+
45
+ ### Paths
46
+
47
+ A path reads from one of three namespaces:
48
+
49
+ - `event.*`: the verified, parsed request body, for example `event.data.object.amount`.
50
+ - `header.*`: an inbound request header, matched case-insensitively, for example `header.x-github-event`.
51
+ - `webhook.*`: endpoint and delivery metadata: `source`, `id`, `deliveryId`, `externalRef`, and `tenantId`. The last two depend on the endpoint configuration.
52
+
53
+ Paths must have non-empty segments. `event.data.amount` is valid; `event.`, `event..amount`, and `event.amount.` are rejected.
54
+
55
+ [Session routing](/webhooks/session-routing) keys use the same namespaces in braces (`{event.data.object.customer}`), and [waiter](/webhooks/waiters) `match` paths use them as keys (`"event.data.object.metadata.orderId"`).
56
+
57
+ ### Operators
58
+
59
+ | Operator | Meaning |
60
+ | --- | --- |
61
+ | `==` `!=` | equality |
62
+ | `>` `<` `>=` `<=` | numeric comparison |
63
+ | `in` `not in` | membership in a list, for example `event.type in ['a', 'b']` |
64
+ | `startsWith` `endsWith` `contains` | string matching |
65
+
66
+ Values are strings in single quotes (`'created'`), numbers (`10000`), booleans (`true`), or `null`. Use a list of these values with `in` and `not in`.
67
+
68
+ ### Comparing two fields
69
+
70
+ Equality and comparison operators also accept a field path on the right-hand side:
71
+
72
+ ```text Filter expression
73
+ event.billing.country == event.shipping.country
74
+ ```
75
+
76
+ ### Matching inside a list
77
+
78
+ `any` matches if at least one array element meets the condition. `all` requires every element to match. The path inside the parentheses is relative to each element:
79
+
80
+ ```text Filter expression
81
+ event.items any ( quantity > 0 )
82
+ ```
83
+
84
+ ### Spacing
85
+
86
+ Separate paths, operators, and values with one space, and put spaces inside grouping and quantifier parentheses, as in `( event.amount > 0 )` and `any ( quantity > 0 )`. Lists and quoted strings may contain spaces (`['opened', 'closed']`, `'Fix the build'`). These forms work with both the TypeScript validator and the deployment parser.
87
+
88
+ ## Filter by event type
89
+
90
+ Use the field or header your provider supplies for its event type. GitHub uses the `x-github-event` header:
91
+
92
+ ```ts /trigger/github-events.ts
93
+ import { webhook, webhooks } from "@trigger.dev/sdk";
94
+
95
+ export const github = webhooks.endpoint.define({ id: "github", source: webhooks.github() });
96
+
97
+ export const onIssuesAndPullRequests = webhook({
98
+ id: "github-issues-and-prs",
99
+ endpoint: github,
100
+ filter: "header.x-github-event in ['issues', 'pull_request']",
101
+ onEvent: async ({ headers }) => {
102
+ console.log("Matched GitHub event", headers.get("x-github-event"));
103
+ },
104
+ });
105
+ ```
106
+
107
+ ## Filtering a session route
108
+
109
+ A `filter` works the same way on [`chat.event`](/webhooks/session-routing): a delivery that doesn't match never reaches the session.
@@ -0,0 +1,149 @@
1
+ ---
2
+ title: "Human-in-the-loop"
3
+ sidebarTitle: "Human-in-the-loop"
4
+ description: "Pause an agent mid-turn to get a human decision from the channel (Slack approve/deny), then resume with the answer."
5
+ ---
6
+
7
+ **An agent on a [channel](/webhooks/channels) can pause a turn to get a human decision, then resume once someone answers in the thread.** It posts controls into the conversation and picks up where it left off once someone clicks, with the decision merged into the turn. This is the channel counterpart of browser [human-in-the-loop](/ai-chat/patterns/human-in-the-loop): the same no-`execute` tool, but the controls live in Slack (or your own surface) instead of a React component. The [`slack()`](/webhooks/slack) connector ships Approve and Deny buttons out of the box.
8
+
9
+ ## How it works
10
+
11
+ The building block is a tool with no `execute` function. When the model calls it, the turn completes with the tool call still pending instead of resolving it. Over a channel the framework then takes over the round-trip:
12
+
13
+ ```mermaid
14
+ sequenceDiagram
15
+ participant U as User
16
+ participant S as Slack
17
+ participant A as Agent run
18
+ U->>S: Message in a thread
19
+ S->>A: Verified delivery starts a turn
20
+ A->>A: Model calls requestApproval (no execute)
21
+ A->>S: renderInteraction posts Approve and Deny
22
+ Note over A: Turn completes, run suspends (no compute while waiting)
23
+ U->>S: Clicks a button
24
+ S->>A: Signed block_actions callback to the same webhook URL
25
+ A->>A: onInteraction resolves the pending tool
26
+ A->>S: finalizeInteraction collapses the buttons
27
+ A->>A: Run resumes, model continues
28
+ A->>S: Reply posts back to the thread
29
+ ```
30
+
31
+ Because it is a no-`execute` pause, the run suspends while it waits rather than holding compute. A human can take minutes or days to decide without burning compute or hitting [`maxDuration`](/runs/max-duration). The mechanics are the same as the browser case, covered in [human-in-the-loop](/ai-chat/patterns/human-in-the-loop#duration-and-cost-while-paused).
32
+
33
+ ## Slack approvals
34
+
35
+ Out of the box, `slack()` posts Approve and Deny buttons for any pending no-`execute` tool. A click resolves to `{ approved: boolean }` and the buttons collapse to the decision. You define the tool and list the channel on the agent:
36
+
37
+ ```ts trigger/support-agent.ts
38
+ import { webhooks } from "@trigger.dev/sdk";
39
+ import { chat } from "@trigger.dev/sdk/ai";
40
+ import { slack, webhookSource } from "@trigger.dev/slack";
41
+ import { tool } from "ai";
42
+ import { anthropic } from "@ai-sdk/anthropic";
43
+ import { z } from "zod";
44
+
45
+ const slackEvents = webhooks.endpoint.define({ id: "slack", source: webhookSource() });
46
+
47
+ // No execute: calling this pauses the turn for a human decision.
48
+ const requestApproval = tool({
49
+ description:
50
+ "Request human approval before a sensitive or irreversible action. " +
51
+ "Call this and stop; you will receive { approved: boolean }, then proceed or decline.",
52
+ inputSchema: z.object({
53
+ action: z.string().describe("A short description of the action needing approval"),
54
+ }),
55
+ });
56
+
57
+ export const supportAgent = chat.agent({
58
+ id: "support-agent",
59
+ channels: [slack({ id: "support-slack", endpoint: slackEvents, token: process.env.SLACK_BOT_TOKEN! })],
60
+ tools: { requestApproval },
61
+ run: async ({ messages, tools, streamText }) =>
62
+ streamText({
63
+ model: anthropic("claude-sonnet-4-5"),
64
+ messages,
65
+ tools,
66
+ system:
67
+ "Before any sensitive or irreversible action (refunds, cancellations, deletions), " +
68
+ "call requestApproval and wait. If approved, confirm it is done; if denied, decline.",
69
+ }),
70
+ });
71
+ ```
72
+
73
+ Button clicks arrive through Slack's interactivity, so the app needs **Interactivity & Shortcuts** turned on with the Request URL set to the **same** endpoint URL as events. The [Slack agents](/webhooks/slack#setup) manifest already does this.
74
+
75
+ When the model calls `requestApproval`, the bot posts "Approval needed" with the action and Approve and Deny buttons, and the session's status is **suspended**. Clicking **Approve** resolves the tool to `{ approved: true }`, the buttons collapse to "Approved by @you", and the agent continues and streams the outcome. **Deny** resolves `{ approved: false }`. Pressing Slack's stop button instead cancels the approval: the buttons are replaced with "Stopped" and a later click resumes nothing.
76
+
77
+ ## Customizing the controls
78
+
79
+ For [`chat.channels.custom`](/webhooks/channels), four hooks own the interaction round-trip:
80
+
81
+ <ParamField body="renderInteraction" type="(pending, ctx) => ChannelMessage | null">
82
+ Map the pending tool call(s) to the controls you post. `pending` is a list of `{ toolCallId, toolName, input }`, and `ctx` carries the `event` and `deliveryId` that started the turn (both absent on a bound turn, such as a `chat.turn()` action answering into the thread). Return `null` to skip posting controls.
83
+ </ParamField>
84
+
85
+ <ParamField body="onInteraction" type="(event) => { toolCallId, output } | null">
86
+ Map a verified callback event to a resolution. The `output` is stitched onto the pending tool call matched by `toolCallId` and the run resumes. Return `null` to treat the event as a normal message instead (a new turn).
87
+ </ParamField>
88
+
89
+ <ParamField body="finalizeInteraction" type="(event, resolution) => void">
90
+ Collapse the controls after a decision so they cannot be clicked again. Best effort: a failure here is logged and the run still resumes.
91
+ </ParamField>
92
+
93
+ <ParamField body="cancelInteraction" type="(cancelled, ctx) => void">
94
+ Take the controls down when a stop (`onControl` returning `{ kind: "stop", whenIdle: "drop" }`) arrives while the session waits on them. Each cancelled call carries the `ref` its controls were posted with. The calls are already resolved as stopped. Best effort: a failure is logged.
95
+ </ParamField>
96
+
97
+ Slack encodes the tool call id and the click's output in each button's `value` so `onInteraction` can resolve the exact call (see [Approvals and buttons](/webhooks/slack#approvals-and-buttons) to customize the buttons or give a tool its own UI), and `finalizeInteraction` posts to the interaction's `response_url` with `replace_original` to swap the buttons for the outcome. On a custom surface you choose the encoding and how you edit the controls away.
98
+
99
+ ```ts
100
+ import { chat } from "@trigger.dev/sdk/ai";
101
+ import { webhooks } from "@trigger.dev/sdk";
102
+
103
+ const surface = webhooks.endpoint.define({
104
+ id: "my-surface",
105
+ source: webhooks.custom<MyEvent>({ /* verifier config */ }),
106
+ });
107
+
108
+ chat.channels.custom({
109
+ id: "my-surface",
110
+ endpoint: surface,
111
+ key: "{event.conversationId}",
112
+ inbound: (e) => e.text,
113
+ send: async (message, ctx) => ({ ref: await postToMySurface(ctx.event, message) }),
114
+ renderInteraction: (pending) => ({
115
+ text: `Approve: ${pending[0].input.action}?`,
116
+ buttons: pending.map((c) => [`${c.toolCallId}:yes`, `${c.toolCallId}:no`]),
117
+ }),
118
+ onInteraction: (e) => {
119
+ const [toolCallId, choice] = e.buttonValue.split(":");
120
+ return { toolCallId, output: { approved: choice === "yes" } };
121
+ },
122
+ finalizeInteraction: async (e) => {
123
+ await editMySurface(e.messageRef, "Decision recorded");
124
+ },
125
+ });
126
+ ```
127
+
128
+ The resolved `output` is whatever your tool expects to receive. The Slack default resolves `{ approved: boolean }`; if your tool needs a richer answer, supply your own `onInteraction` that returns the shape your tool reads.
129
+
130
+ ## The reply after a decision
131
+
132
+ When the turn resumes, the channel reply shows the agent's answer, not the text it produced before pausing. The framework posts the assistant text generated after the tool call, so a preamble like "I'll need approval first" is not concatenated onto the final "done" message. The approval prompt itself already lives in the collapsed controls.
133
+
134
+ ## Next steps
135
+
136
+ <CardGroup cols={2}>
137
+ <Card title="Channels" icon="comments" href="/webhooks/channels">
138
+ Point Slack or any surface at an agent so messages become turns.
139
+ </Card>
140
+ <Card title="Browser human-in-the-loop" icon="hand" href="/ai-chat/patterns/human-in-the-loop">
141
+ The same no-execute pause, rendered in a React frontend.
142
+ </Card>
143
+ <Card title="Sessions" icon="layer-group" href="/ai-chat/sessions">
144
+ The durable per-conversation state a channel turn runs on.
145
+ </Card>
146
+ <Card title="Filters" icon="filter" href="/webhooks/filters">
147
+ Gate which verified deliveries reach the agent.
148
+ </Card>
149
+ </CardGroup>
@@ -0,0 +1,185 @@
1
+ ---
2
+ title: "Linear agents"
3
+ description: "Turn a chat.agent into a Linear agent: assign it an issue or @mention it, and it works the issue inside Linear's agent session UI."
4
+ sidebarTitle: "Linear agents"
5
+ ---
6
+
7
+ Linear opens an **agent session** when someone @mentions your app in an issue or delegates (assigns) an issue to it. `linear()` from `@trigger.dev/linear` is a [channel](/webhooks/channels) connector for those sessions: each one becomes a durable Trigger.dev [session](/ai-chat/sessions), the user's prompt runs as a turn, and the agent's work shows up in Linear as thoughts, actions, a plan, an approval card when it needs a decision, and a response when it is done.
8
+
9
+ ```ts
10
+ import { webhooks } from "@trigger.dev/sdk";
11
+ import { chat } from "@trigger.dev/sdk/ai";
12
+ import { linear, webhookSource } from "@trigger.dev/linear";
13
+ import { anthropic } from "@ai-sdk/anthropic";
14
+
15
+ const linearEvents = webhooks.endpoint.define({ id: "linear", source: webhookSource() });
16
+
17
+ export const triage = chat.agent({
18
+ id: "triage",
19
+ channels: [linear({ id: "linear", endpoint: linearEvents, token: process.env.LINEAR_ACCESS_TOKEN! })],
20
+ tools: { searchDocs, createSubIssues },
21
+ run: async ({ messages, tools, signal, streamText }) =>
22
+ streamText({ model: anthropic("claude-sonnet-4-5"), messages, tools, abortSignal: signal }),
23
+ });
24
+ ```
25
+
26
+ The `run()` loop is the same one a browser chat or a Slack thread uses. Verification, the five second webhook acknowledgment, the first activity Linear expects within ten seconds, per-session state, dedup of Linear's retries, replay and the transcript are handled for you.
27
+
28
+ ## Setup
29
+
30
+ <Steps>
31
+ <Step title="Create the OAuth app">
32
+ In Linear, create an OAuth application under **Settings, API**. Enable **Webhooks** on it and turn on the **Agent session events** category (Inbox notifications and Permission changes are useful too). Copy the webhook **signing secret**.
33
+ </Step>
34
+ <Step title="Install it as an agent">
35
+ Authorize the app with `actor=app` in the authorization URL and request the `app:assignable` and `app:mentionable` scopes, so members can assign issues to it and @mention it. Installation needs a workspace admin. Keep the resulting access token; pass it as `token`.
36
+ </Step>
37
+ <Step title="Deploy and connect the endpoint">
38
+ Declare the endpoint with `webhookSource()` from `@trigger.dev/linear` and pass it to `linear({ endpoint })`. Deploying registers the hosted [endpoint](/webhooks/connect). Paste the app's webhook signing secret as the endpoint's signing secret, and set the app's webhook URL to the endpoint's URL. The endpoint's setup prompt walks an AI coding agent through these settings.
39
+ </Step>
40
+ <Step title="Try it">
41
+ Open an issue and assign it to the app, or write a comment that @mentions it. Linear shows the agent's first thought within a few seconds and then its work.
42
+ </Step>
43
+ </Steps>
44
+
45
+ ## What the agent sees
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.
48
+
49
+ A follow-up prompt on the session is just the message. If the user hits **Stop** in Linear, see [Stopping](#stopping).
50
+
51
+ Images and PDFs uploaded to Linear reach the model as files: screenshots in the prompt, in the comment that mentioned the agent, or in the issue description. The transcript keeps the upload link, and the `streamText` passed to `run()` downloads the file with the app's token when the model reads it, at most ten per message. A file that can't be downloaded reaches the model as a short note. A custom `inbound` keeps this by adding `linearFileParts(markdown)` to its message parts.
52
+
53
+ Turn any of that off with `context`:
54
+
55
+ ```ts
56
+ linear({ id: "linear", endpoint: linearEvents, token, context: { guidance: false, promptContext: true, previousComments: true } })
57
+ ```
58
+
59
+ ## What Linear shows
60
+
61
+ | Moment | Activity in Linear |
62
+ | --- | --- |
63
+ | Turn starts | An ephemeral `thought` ("Looking into this...") |
64
+ | A tool is called | An ephemeral `action` with the tool name and input, then the same action with its `result` |
65
+ | The reply is ready | A `response` with the reply as markdown |
66
+ | The turn fails | An `error` |
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
+ | The user picks an option | A `thought` recording the decision ("Approved by Jane.", "Jane chose "High".") |
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)) |
70
+
71
+ Tool mirroring is on by default; pass `progress: "off"` to post only the response. Bare Linear URLs in a reply become mentions, so link issues and documents by URL.
72
+
73
+ ## The plan
74
+
75
+ Linear shows an agent's plan as a checklist under the conversation. The model owns it: add `linearPlanTool()` to the agent's tools and ask for a plan in the system prompt, and the model sets it when it starts and updates it as steps finish. The whole list is replaced on each call. On a turn that is not a Linear session the tool reports that nothing was updated, so `run()` needs no Linear-specific code.
76
+
77
+ ```ts
78
+ import { linear, linearPlanTool } from "@trigger.dev/linear";
79
+
80
+ export const triage = chat.agent({
81
+ id: "triage",
82
+ channels: [linear({ id: "linear", endpoint: linearEvents, token })],
83
+ tools: { searchDocs, createSubIssues, updatePlan: linearPlanTool() },
84
+ run: async ({ messages, tools, signal, streamText }) =>
85
+ streamText({
86
+ model,
87
+ messages,
88
+ tools,
89
+ abortSignal: signal,
90
+ system: "Start by setting a short plan with updatePlan and keep it current as you work.",
91
+ }),
92
+ });
93
+ ```
94
+
95
+ ## Helpers
96
+
97
+ For manual control, `chat.surface<LinearRunSurface>()` returns the current turn's helpers from anywhere inside the run, including a tool's `execute`. The same object is handed to `run()` as `channel`.
98
+
99
+ | Helper | Effect |
100
+ | --- | --- |
101
+ | `plan(steps)` | Replace the session's plan. Each step is `{ content, status }` with status `pending`, `inProgress`, `completed` or `canceled`. What `linearPlanTool()` calls. |
102
+ | `thought(text, { ephemeral })` | Post a thought. |
103
+ | `action(action, parameter, result?)` | Post an action, optionally with its result. |
104
+ | `link(label, url)` | Add an external link to the session header (a pull request, a dashboard). |
105
+ | `summary(text)` | Set the one-line session summary. |
106
+ | `session` | The bound agent session and issue ids. |
107
+
108
+ ## Approvals and choices
109
+
110
+ A tool with no `execute` pauses the turn for a human decision, exactly as on [any channel](/webhooks/human-in-the-loop). On Linear the connector posts an elicitation card with Approve and Deny options. The card lays the tool input out as markdown: a list of proposed items becomes a numbered list with a bold title per item, a flat object becomes key and value bullets, and only shapes it cannot lay out fall back to JSON. For a card written for the tool, pass a renderer per tool name:
111
+
112
+ ```ts
113
+ linear({
114
+ id: "linear",
115
+ endpoint: linearEvents,
116
+ token,
117
+ approvals: {
118
+ createSubIssues: (input) => ({
119
+ title: "Create these sub-issues?",
120
+ body: (input as { issues: { title: string }[] }).issues.map((i) => `- ${i.title}`).join("\n"),
121
+ }),
122
+ },
123
+ })
124
+ ```
125
+
126
+ The user picks one and presses Continue; the choice shows in the session as "Approve createSubIssues [call_a]" (the tool and the call it applies to, so the decision always lands on the call whose input the card shows) and resumes the run with `{ approved: true | false }`, and the decision is recorded as a thought. Typing a free-text reply instead dismisses the options and runs as a normal turn, so the model can take the reply into account (and ask again) with the approval still pending.
127
+
128
+ `interactions` gives a tool its own choices instead of Approve and Deny. The card is a select of `options`, and the chosen option resolves the tool to `{ selected }`:
129
+
130
+ ```ts
131
+ linear({
132
+ id: "linear",
133
+ endpoint: linearEvents,
134
+ token,
135
+ interactions: {
136
+ pickPriority: (call) => ({
137
+ title: "What priority should this be?",
138
+ body: (call.input as { reason: string }).reason,
139
+ options: ["Urgent", "High", "Medium", "Low"],
140
+ }),
141
+ },
142
+ })
143
+ ```
144
+
145
+ The choice shows in the session as "High [choice:call_a]", resumes the run with `{ selected: "High" }`, and is recorded as a thought.
146
+
147
+ ## Stopping
148
+
149
+ Pressing Stop in Linear sends the session a prompt with the `stop` signal. While a turn is running, the connector aborts it through the agent's stop signal (the same one the browser's stop button uses): generation halts, the text written so far posts as the response with `stopped: true`, and the stop prompt itself is consumed. Pressed while the agent is idle, the prompt runs as a turn that tells the model to stop taking actions and summarize. A Stop pressed while a turn was running that arrives after the turn finished is consumed, so pressing Stop twice never adds a wind-down turn.
150
+
151
+ ## Prompts sent while the agent is working
152
+
153
+ A prompt sent while the agent is still working gets a thought as soon as it arrives ("Got it, I'll take that into account."), so the user knows it was picked up. It then waits and runs as the next turn. With [`pendingMessages`](/ai-chat/pending-messages) on the agent, `shouldInject` can fold it into the work in progress at the next step instead; a prompt it declines still runs as its own turn. `receivedThought` changes the text, takes a function of the event for wording per prompt, and `null` posts nothing; since the prompt may be answered either way, keep the wording true for both. Stop and the approval card's options are never acknowledged or folded in.
154
+
155
+ ## Answering into the session from elsewhere
156
+
157
+ The first Linear turn binds the session to the agent session ([bound sessions](/webhooks/channels#bound-sessions)). A [`chat.event`](/webhooks/session-routing) routed to the same session key, a Sentry alert keyed on the issue identifier for example, can return `chat.turn()` from `onAction` and its answer posts into the Linear session as a response, with no Linear event involved. Linear reopens a completed session when new activity arrives.
158
+
159
+ ## Options
160
+
161
+ | Option | Type | Description |
162
+ | --- | --- | --- |
163
+ | `id` | `string` | Connector id, unique per agent. |
164
+ | `endpoint` | endpoint | The Linear endpoint, declared with `webhooks.endpoint.define({ id, source: webhookSource() })`. Other `webhook()` tasks can subscribe to the same endpoint. |
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
+ | `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. |
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
+ | `progress` | `"actions"` or `"off"` | Mirror tool calls as action activities (default) or post only the response. |
171
+ | `summarize` | `boolean` | Set the session title from each response's first line. Off by default; Linear titles the session itself. |
172
+ | `approvals` | `{ [toolName]: (input, call) => { title?, body } }` | Custom approval card body per tool. Default lays the tool input out as markdown. |
173
+ | `interactions` | `{ [toolName]: (call) => { title?, body, options } }` | A tool's own select card; the chosen option resolves the tool to `{ selected }`. |
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
+ | `ack` | message, `null`, or function | The first thought. Pass `null` to post nothing before the response. |
176
+ | `inbound` / `outbound` | functions | Map the event to the turn, and the reply to an activity. |
177
+ | `delivery` | `"final"` or `"stream"` | `"final"` (default) posts the response when the turn ends. `"stream"` also posts ephemeral thoughts as the reply streams. |
178
+ | `apiUrl` | `string` | Override the Linear GraphQL URL, for testing against a mock. Files on its origin are treated as Linear uploads. |
179
+
180
+ <Note>
181
+ Linear expects the first activity within ten seconds of opening a session. The connector posts it at
182
+ the top of the run, so on a cold start close to that limit the session can show as unresponsive for a
183
+ moment; it recovers as soon as the thought lands. Linear retries a failed delivery after one minute,
184
+ one hour and six hours, and the endpoint deduplicates those retries.
185
+ </Note>
@@ -0,0 +1,99 @@
1
+ ---
2
+ title: "Webhooks overview"
3
+ description: "Receive provider webhooks on hosted, verified endpoints and route each event to tasks, agents, and waiting runs."
4
+ sidebarTitle: "Overview"
5
+ ---
6
+
7
+ **A webhook endpoint is one hosted URL that you register with a provider.** Trigger.dev verifies every request against the endpoint's [source](/webhooks/sources), records it as a [delivery](/webhooks/deliveries), and hands the event to everything subscribed to the endpoint.
8
+
9
+ Declare the endpoint once, then subscribe to it:
10
+
11
+ ```ts /trigger/stripe.ts
12
+ import { webhook, webhooks } from "@trigger.dev/sdk";
13
+
14
+ export const stripe = webhooks.endpoint.define({
15
+ id: "stripe",
16
+ source: webhooks.stripe(),
17
+ });
18
+
19
+ export const fulfillOrder = webhook({
20
+ id: "fulfill-order",
21
+ endpoint: stripe,
22
+ filter: "event.type == 'checkout.session.completed'",
23
+ onEvent: async ({ event }) => {
24
+ console.log("Fulfilling", event.data.object.id);
25
+ },
26
+ });
27
+
28
+ export const recordRefund = webhook({
29
+ id: "record-refund",
30
+ endpoint: stripe,
31
+ filter: "event.type == 'charge.refunded'",
32
+ onEvent: async ({ event, headers, endpoint, ctx }) => {
33
+ console.log("Refund", event.id, "on", endpoint?.id, "in run", ctx.run.id);
34
+ },
35
+ });
36
+ ```
37
+
38
+ Stripe gets one URL. A checkout event runs `fulfill-order`, a refund runs `record-refund`, and anything else is recorded as `FILTERED`.
39
+
40
+ ## What can subscribe
41
+
42
+ | Subscriber | Declared with | What a delivery does |
43
+ | --- | --- | --- |
44
+ | Task | `webhook({ endpoint, onEvent })` | Starts a run of the task with the event. |
45
+ | Agent session | [`chat.event({ endpoint, key })`](/webhooks/session-routing) listed on a `chat.agent` | Delivers an action to the session the `key` names. |
46
+ | Chat frontend | a [channel connector](/webhooks/channels) such as `chat.channels.custom({ endpoint })` | Runs a turn on the conversation's session and posts the reply back. |
47
+ | Waiting run | [`webhooks.waiter.create(endpoint, { match })`](/webhooks/waiters) | Resumes a run that is waiting for this event. |
48
+
49
+ Each subscriber has its own [filter](/webhooks/filters), so one delivery can reach several subscribers and skip the rest. An endpoint can have up to 25 subscribers. An endpoint with no subscribers still gets a URL, so you can register it with the provider before writing the handlers.
50
+
51
+ ## The handler
52
+
53
+ `onEvent` receives:
54
+
55
+ | Argument | Value |
56
+ | --- | --- |
57
+ | `event` | The verified request body, parsed as JSON. Its TypeScript type comes from the [source](/webhooks/sources#typing-the-event). |
58
+ | `headers` | Request headers as a Web `Headers` object, with case-insensitive `get()` and `has()` methods. Credentials such as signatures are removed. |
59
+ | `endpoint` | The endpoint the delivery arrived on: `id`, `declaredId`, `tenantId`, `externalRef`, and the `metadata` you declared. |
60
+ | `ctx` | The same [run context](/context) that regular tasks receive. |
61
+
62
+ `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
+
64
+ ## How it works
65
+
66
+ The CLI finds your endpoints and subscribers during `trigger dev` and deployment, and each environment gets one endpoint per declared id. The endpoint holds the URL and the credential used to verify requests.
67
+
68
+ A request must pass verification before anything else happens. Provider handshakes are answered directly and create no delivery. A verified request is checked for duplicates, recorded, and then routed: each subscriber's filter is checked, matching subscribers run, and waiting runs whose match fits the event are resumed. The delivery records a result per subscriber.
69
+
70
+ ## Connect it
71
+
72
+ Every endpoint can produce a setup prompt for an AI coding agent. It holds the URL, the events your subscribers filter on, and the steps for registering the endpoint and its signing secret with the provider. Copy it from the endpoints list or the endpoint's page, or read it with the MCP server's `get_webhook_endpoint_details` tool. See [Connect a provider](/webhooks/connect).
73
+
74
+ <CardGroup cols={2}>
75
+ <Card title="Connect a provider" icon="plug" href="/webhooks/connect">
76
+ Register the URL and signing secret, then send a test event.
77
+ </Card>
78
+ <Card title="Sources and verification" icon="shield-check" href="/webhooks/sources">
79
+ Choose a provider source or define a custom verification scheme.
80
+ </Card>
81
+ <Card title="Filters" icon="filter" href="/webhooks/filters">
82
+ Choose which events reach each subscriber.
83
+ </Card>
84
+ <Card title="Waiters" icon="hourglass" href="/webhooks/waiters">
85
+ Pause a run until a matching webhook arrives.
86
+ </Card>
87
+ <Card title="Deliveries and endpoints" icon="inbox" href="/webhooks/deliveries">
88
+ Inspect received events and where each one went.
89
+ </Card>
90
+ <Card title="Session routing" icon="arrows-to-dot" href="/webhooks/session-routing">
91
+ Route deliveries to a durable per-key agent session.
92
+ </Card>
93
+ <Card title="Channels" icon="comments" href="/webhooks/channels">
94
+ Turn a webhook into a chat frontend for an agent.
95
+ </Card>
96
+ <Card title="Human-in-the-loop" icon="user-check" href="/webhooks/human-in-the-loop">
97
+ Approvals and interactive controls over a channel.
98
+ </Card>
99
+ </CardGroup>