-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(sdk,slack): webhook sources, agent channels, and human-in-the-loop #4537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
+2,940
−12
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| --- | ||
| "@trigger.dev/core": minor | ||
| "@trigger.dev/sdk": minor | ||
| "@trigger.dev/slack": minor | ||
| "trigger.dev": minor | ||
| --- | ||
|
|
||
| Add hosted webhooks: receive and verify provider webhooks as a task, with no ingress or verification code of your own. | ||
|
|
||
| - `webhook()` declares an endpoint that routes a verified, typed event to an `onEvent` handler. Choose a source with a preset (`webhooks.stripe()`, `webhooks.github()`, and others) or `webhooks.custom<T>(config)`. Declared webhooks are discovered like tasks and synced to a hosted URL on deploy. | ||
| - `filter` gates which deliveries run, using a type-safe expression checked against the event at author time (`event.`/`header.`/`webhook.` paths, `&&`/`||`, comparison and `in`/`contains` operators, field-to-field comparison, and array quantifiers). A non-matching delivery is still recorded, not routed. | ||
| - `chat.event({ source, key, type })` routes deliveries that share a `key` to one durable session (per customer, installation, or issue) and delivers them to an agent's `onAction` as a typed envelope. | ||
| - Channels turn a chat surface into an agent frontend: `chat.channels.custom({ source, key, inbound, send })`, or the new `@trigger.dev/slack` package's `slack()` (Slack Events API verification, per-thread sessions, `chat.postMessage`/`chat.update` egress, `mentions()`, `startOn`, lifecycle reactions). Inbound messages run as turns and the reply posts back. Human-in-the-loop is built in: a tool with no `execute` pauses the turn, the connector posts controls (Slack ships Approve / Deny buttons), and a verified click resolves the tool and resumes the run. | ||
| - HTTP API for listing webhook endpoints and deliveries, plus rotate-secret, enable/disable, and replay. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| --- | ||
| title: "Channels (chat frontends)" | ||
| description: "Point Slack (or any chat surface) at an agent: messages become turns and replies post back." | ||
| sidebarTitle: "Channels" | ||
| --- | ||
|
|
||
| A [session route](/webhooks/session-routing) delivers a verified event to an agent as an [action](/ai-chat/actions): the agent reacts, and the response is a side effect. A **channel** is the other half: the webhook IS the chat surface. Inbound messages become **turns** (the normal `run()` loop), and the agent's reply is posted **back** to the surface. A Slack thread becomes a real conversation with the agent, exactly like the browser chat, just a different frontend. | ||
|
|
||
| List channels on a [`chat.agent`](/ai-chat/overview) alongside (or instead of) `events`: | ||
|
|
||
| ```ts | ||
| import { chat } from "@trigger.dev/sdk/ai"; | ||
| import { slack } from "@trigger.dev/slack"; | ||
|
|
||
| export const supportAgent = chat.agent({ | ||
| id: "support-agent", | ||
| channels: [slack({ id: "support-slack", token: process.env.SLACK_BOT_TOKEN! })], | ||
| run: async ({ messages }) => streamText({ model: anthropic("claude-sonnet-4-5"), messages }), | ||
| }); | ||
| ``` | ||
|
|
||
| The `run()` loop is unchanged: the agent does not know or care that it is talking to Slack. One verified Slack message in a thread is routed to a durable [session](/ai-chat/sessions) keyed to that thread, run as a turn, and the reply is posted into the thread. | ||
|
|
||
| ## Slack | ||
|
|
||
| `slack()` (from `@trigger.dev/slack`) is a channel connector: it verifies inbound Slack events, maps a message to the turn, and posts the reply back with `chat.postMessage` / `chat.update`. | ||
|
|
||
| <Steps> | ||
| <Step title="Create a Slack app"> | ||
| Create an app at [api.slack.com/apps](https://api.slack.com/apps). Add the `chat:write` bot scope and install it to your workspace to get a bot token (`xoxb-...`). | ||
| </Step> | ||
| <Step title="Deploy the agent + connect the endpoint"> | ||
| Deploying registers a hosted [endpoint](/webhooks/connect) for the channel. Set its signing secret to your Slack app's **Signing Secret**, and pass the bot token as `token`. | ||
| </Step> | ||
| <Step title="Subscribe to events"> | ||
| In the app's **Event Subscriptions**, set the request URL to the endpoint's webhook URL. Slack sends a one-time `url_verification` handshake, which the endpoint answers automatically. Subscribe the bot to `message.channels`, then invite the bot to the channel (`/invite @yourapp`). | ||
| </Step> | ||
| </Steps> | ||
|
|
||
| By default `slack()` keys one session per thread, strips the leading bot mention from the message, posts an "on it..." placeholder while the agent works, and edits it to the answer. Override any of that: | ||
|
|
||
| ```ts | ||
| slack({ | ||
| id: "support-slack", | ||
| token: process.env.SLACK_BOT_TOKEN!, | ||
| // ignore anything but questions (composed with the built-in self-message guard) | ||
| filter: "event.event.text contains '?'", | ||
| inbound: (e) => e.event?.text ?? "", | ||
| outbound: (reply) => ({ text: reply.text }), | ||
| ack: (e) => ({ text: "thinking..." }), // pass `null` to post only the final answer | ||
| }); | ||
| ``` | ||
|
|
||
| <Note> | ||
| `slack()` always drops the bot's own messages (and their edits) before they reach the agent, so the | ||
| agent never replies to itself. A multi-workspace app can pass a `token` resolver keyed on the event's | ||
| team instead of a single string. | ||
| </Note> | ||
|
|
||
| ### Summoning with a mention | ||
|
|
||
| By default `slack()` starts (or resumes) a session for every non-bot message in a subscribed channel. To make the agent respond only when it is @mentioned, pass `startOn` with the `mentions` helper. The first mention in a thread starts the session, and the agent then follows the rest of the thread without needing to be mentioned again. | ||
|
|
||
| ```ts | ||
| import { slack, mentions } from "@trigger.dev/slack"; | ||
|
|
||
| slack({ | ||
| id: "support-slack", | ||
| token: process.env.SLACK_BOT_TOKEN!, | ||
| startOn: mentions("U012BOT"), // your bot's user id (pass several for multiple bots) | ||
| }); | ||
| ``` | ||
|
|
||
| ### Reacting to messages | ||
|
|
||
| `slack()` can add an emoji reaction to the triggering message to signal progress. Set `reactions` with any of `working`, `done`, and `error`: the connector adds `working` when the turn starts, swaps it to `done` when the turn finishes, and reacts with `error` if it fails. This needs the `reactions:write` scope. | ||
|
|
||
| ```ts | ||
| slack({ | ||
| id: "support-slack", | ||
| token: process.env.SLACK_BOT_TOKEN!, | ||
| reactions: { working: "eyes", done: "white_check_mark", error: "warning" }, | ||
| }); | ||
| ``` | ||
|
|
||
| ### Options | ||
|
|
||
| | Option | Type | Description | | ||
| | --- | --- | --- | | ||
| | `id` | `string` | Connector id, unique per agent. | | ||
| | `token` | `string` or resolver | Bot token (`xoxb-...`), or a function of the event's team for multi-workspace apps. | | ||
| | `key` | `string` | Session [key](/webhooks/session-routing) template. Defaults to one session per thread. | | ||
| | `filter` | `string` | Extra [filter](/webhooks/filters), composed with the built-in self-message guard. | | ||
| | `startOn` | `string` | Only start a session when the event matches (see `mentions`). Existing sessions always resume. | | ||
| | `ack` | message, `null`, or function | Placeholder posted while the agent works. Pass `null` to post only the final answer. | | ||
| | `reactions` | `{ working?, done?, error? }` | Lifecycle emoji reactions on the triggering message. | | ||
| | `inbound` / `outbound` | functions | Map the Slack event to the turn, and the reply to a Slack message. | | ||
| | `delivery` | `"final"` or `"stream"` | `"final"` (default) posts a placeholder and edits it to the answer. `"stream"` edits live as the reply streams. | | ||
| | `apiBaseUrl` | `string` | Override the Slack Web API base, for testing against a mock. | | ||
|
|
||
| ## Approvals and interactive controls | ||
|
|
||
| An agent on a channel can pause a turn to get a human decision, approving a refund or confirming a deletion, and resume once someone clicks a button in the thread. `slack()` renders Approve / Deny buttons for you and collapses them to the decision once clicked. See [human-in-the-loop](/webhooks/human-in-the-loop). | ||
|
|
||
| ## Any surface: `chat.channels.custom` | ||
|
|
||
| For a surface without a preset, `chat.channels.custom` is the generic connector. You supply the [source](/webhooks/sources) to verify, the session `key`, the `inbound` map, and the egress `send`: | ||
|
|
||
| ```ts | ||
| import { chat } from "@trigger.dev/sdk/ai"; | ||
| import { webhooks } from "@trigger.dev/sdk"; | ||
|
|
||
| const mySurface = chat.channels.custom({ | ||
| id: "my-surface", | ||
| source: webhooks.custom<MyEvent>({ /* verifier config */ }), | ||
| key: "{body.conversationId}", | ||
| inbound: (e) => e.text, | ||
| outbound: (reply) => (reply.text ? { text: reply.text } : null), // null posts nothing | ||
| send: async (message, ctx) => { | ||
| const ref = await postToMySurface(ctx.event, message.text, ctx.previousRef); | ||
| return { ref }; // an existing ref means edit-in-place on the next turn | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| `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). | ||
|
|
||
| ## Channels vs events | ||
|
|
||
| Both are inbound surfaces on a `chat.agent`, and an agent can list both: | ||
|
|
||
| - [`events`](/webhooks/session-routing) (`chat.event`): the webhook is a signal. Delivered to `onAction`; the agent acts, no reply is sent back. | ||
| - `channels` (`slack`, `chat.channels.custom`): the webhook is a chat frontend. Delivered as a turn to `run()`; the reply is posted back. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| --- | ||
| title: "Connecting a provider" | ||
| description: "Point a provider at the webhook URL and set the signing secret." | ||
| sidebarTitle: "Connecting a provider" | ||
| --- | ||
|
|
||
| When you deploy (or run `dev`), each webhook task gets an **endpoint** with a unique, unguessable webhook URL. Open the webhook in the dashboard, go to **Endpoints**, and open the endpoint to find its **Connect** panel. | ||
|
|
||
| <Steps> | ||
| <Step title="Copy the webhook URL"> | ||
| Copy it from the endpoint's Connect panel. On Trigger.dev Cloud it looks like | ||
| `https://webhooks.trigger.dev/webhooks/v1/ingest/<id>`. A self-hosted instance serves it from that | ||
| instance's own base URL. This is what you give the provider as its webhook destination. | ||
| </Step> | ||
| <Step title="Set the signing secret"> | ||
| A webhook can't accept deliveries until its signing secret is set. Until then every request is | ||
| rejected. There are two flows, and the Connect panel shows the right one for the provider: | ||
|
|
||
| - **The provider generates the secret** (Stripe, Svix): copy it from the provider and paste it | ||
| into **Set secret**. | ||
| - **You choose the secret** (GitHub, or a service you control): click **Generate secret** and | ||
| Trigger.dev mints a strong secret and shows it once. Paste that into the provider's webhook config. | ||
| </Step> | ||
| <Step title="Point the provider at the webhook URL"> | ||
| Add the webhook URL as the destination in your provider's dashboard. The Connect panel | ||
| shows the exact signature scheme (header, algorithm, signing string) the provider should use. | ||
| </Step> | ||
| </Steps> | ||
|
|
||
| <Warning> | ||
| The signing secret is stored encrypted and is never shown again after it's set. To rotate it, | ||
| use **Rotate secret** (or **Regenerate**) and update the provider with the new value. | ||
| </Warning> | ||
|
|
||
| Once a provider is sending events, watch them arrive on the [Deliveries](/webhooks/deliveries) page, which also explains what an [endpoint](/webhooks/deliveries#endpoints) is. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Slack API message.channels events required scopes channels:history💡 Result:
To receive the message.channels event via the Slack Events API, your app must possess the channels:history OAuth scope [1][2]. This scope allows your app to view messages and other content in public channels that it has been added to [3][4]. When subscribing to message events, the specific event type dictates the required scope [1]: - message.channels: Requires channels:history [2]. - message.groups: Requires groups:history [1]. - message.im: Requires im:history [1]. - message.mpim: Requires mpim:history [1]. Please note that for your app to receive these events, it must be a member of the relevant channel [1][5]. If you intend to receive messages from private channels, groups, or direct messages, you must subscribe to the corresponding event type and request the appropriate history scope for that conversation type [1].
Citations:
🌐 Web query:
Slack API message.channels events scopes channels:history chat:write💡 Result:
The message.channels event is an Event API type in Slack used to receive messages posted within public channels [1][2]. To use this event and perform related actions, your application requires specific OAuth scopes [3]. Required Scopes: channels:history: This scope is required to subscribe to the message.channels event [2][4]. It grants your app the permission to view messages and other content in public channels that the app has been added to [5]. chat:write: This scope is required to send messages on behalf of your app, for example, using the chat.postMessage API method [6][7]. It allows your app to post messages in the channels and contexts where it has been granted access [8]. In summary, subscribing to the message.channels event necessitates the channels:history scope to receive the data [4][9], while the chat:write scope is necessary if your application intends to respond to those events by sending new messages [10][7].
Citations:
Add the required Slack event-read scope.
message.channelsrequireschannels:history;chat:writeonly enables replies. Addchannels:historyto Step “Create a Slack app” and tell readers to reinstall the app after changing scopes.