|
| 1 | +--- |
| 2 | +title: "Channels (chat frontends)" |
| 3 | +description: "Point Slack (or any chat surface) at an agent: messages become turns and replies post back." |
| 4 | +sidebarTitle: "Channels" |
| 5 | +--- |
| 6 | + |
| 7 | +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. |
| 8 | + |
| 9 | +List channels on a [`chat.agent`](/ai-chat/overview) alongside (or instead of) `events`: |
| 10 | + |
| 11 | +```ts |
| 12 | +import { chat } from "@trigger.dev/sdk/ai"; |
| 13 | +import { slack } from "@trigger.dev/slack"; |
| 14 | + |
| 15 | +export const supportAgent = chat.agent({ |
| 16 | + id: "support-agent", |
| 17 | + channels: [slack({ id: "support-slack", token: process.env.SLACK_BOT_TOKEN! })], |
| 18 | + run: async ({ messages }) => streamText({ model: anthropic("claude-sonnet-4-5"), messages }), |
| 19 | +}); |
| 20 | +``` |
| 21 | + |
| 22 | +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. |
| 23 | + |
| 24 | +## Slack |
| 25 | + |
| 26 | +`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`. |
| 27 | + |
| 28 | +<Steps> |
| 29 | + <Step title="Create a Slack app"> |
| 30 | + 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-...`). |
| 31 | + </Step> |
| 32 | + <Step title="Deploy the agent + connect the endpoint"> |
| 33 | + 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`. |
| 34 | + </Step> |
| 35 | + <Step title="Subscribe to events"> |
| 36 | + 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`). |
| 37 | + </Step> |
| 38 | +</Steps> |
| 39 | + |
| 40 | +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: |
| 41 | + |
| 42 | +```ts |
| 43 | +slack({ |
| 44 | + id: "support-slack", |
| 45 | + token: process.env.SLACK_BOT_TOKEN!, |
| 46 | + // ignore anything but questions (composed with the built-in self-message guard) |
| 47 | + filter: "event.event.text contains '?'", |
| 48 | + inbound: (e) => e.event?.text ?? "", |
| 49 | + outbound: (reply) => ({ text: reply.text }), |
| 50 | + ack: (e) => ({ text: "thinking..." }), // pass `null` to post only the final answer |
| 51 | +}); |
| 52 | +``` |
| 53 | + |
| 54 | +<Note> |
| 55 | + `slack()` always drops the bot's own messages (and their edits) before they reach the agent, so the |
| 56 | + agent never replies to itself. A multi-workspace app can pass a `token` resolver keyed on the event's |
| 57 | + team instead of a single string. |
| 58 | +</Note> |
| 59 | + |
| 60 | +### Summoning with a mention |
| 61 | + |
| 62 | +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. |
| 63 | + |
| 64 | +```ts |
| 65 | +import { slack, mentions } from "@trigger.dev/slack"; |
| 66 | + |
| 67 | +slack({ |
| 68 | + id: "support-slack", |
| 69 | + token: process.env.SLACK_BOT_TOKEN!, |
| 70 | + startOn: mentions("U012BOT"), // your bot's user id (pass several for multiple bots) |
| 71 | +}); |
| 72 | +``` |
| 73 | + |
| 74 | +### Reacting to messages |
| 75 | + |
| 76 | +`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. |
| 77 | + |
| 78 | +```ts |
| 79 | +slack({ |
| 80 | + id: "support-slack", |
| 81 | + token: process.env.SLACK_BOT_TOKEN!, |
| 82 | + reactions: { working: "eyes", done: "white_check_mark", error: "warning" }, |
| 83 | +}); |
| 84 | +``` |
| 85 | + |
| 86 | +### Options |
| 87 | + |
| 88 | +| Option | Type | Description | |
| 89 | +| --- | --- | --- | |
| 90 | +| `id` | `string` | Connector id, unique per agent. | |
| 91 | +| `token` | `string` or resolver | Bot token (`xoxb-...`), or a function of the event's team for multi-workspace apps. | |
| 92 | +| `key` | `string` | Session [key](/webhooks/session-routing) template. Defaults to one session per thread. | |
| 93 | +| `filter` | `string` | Extra [filter](/webhooks/filters), composed with the built-in self-message guard. | |
| 94 | +| `startOn` | `string` | Only start a session when the event matches (see `mentions`). Existing sessions always resume. | |
| 95 | +| `ack` | message, `null`, or function | Placeholder posted while the agent works. Pass `null` to post only the final answer. | |
| 96 | +| `reactions` | `{ working?, done?, error? }` | Lifecycle emoji reactions on the triggering message. | |
| 97 | +| `inbound` / `outbound` | functions | Map the Slack event to the turn, and the reply to a Slack message. | |
| 98 | +| `delivery` | `"final"` or `"stream"` | `"final"` (default) posts a placeholder and edits it to the answer. `"stream"` edits live as the reply streams. | |
| 99 | +| `apiBaseUrl` | `string` | Override the Slack Web API base, for testing against a mock. | |
| 100 | + |
| 101 | +## Approvals and interactive controls |
| 102 | + |
| 103 | +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). |
| 104 | + |
| 105 | +## Any surface: `chat.channels.custom` |
| 106 | + |
| 107 | +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`: |
| 108 | + |
| 109 | +```ts |
| 110 | +import { chat } from "@trigger.dev/sdk/ai"; |
| 111 | +import { webhooks } from "@trigger.dev/sdk"; |
| 112 | + |
| 113 | +const mySurface = chat.channels.custom({ |
| 114 | + id: "my-surface", |
| 115 | + source: webhooks.custom<MyEvent>({ /* verifier config */ }), |
| 116 | + key: "{body.conversationId}", |
| 117 | + inbound: (e) => e.text, |
| 118 | + outbound: (reply) => (reply.text ? { text: reply.text } : null), // null posts nothing |
| 119 | + send: async (message, ctx) => { |
| 120 | + const ref = await postToMySurface(ctx.event, message.text, ctx.previousRef); |
| 121 | + return { ref }; // an existing ref means edit-in-place on the next turn |
| 122 | + }, |
| 123 | +}); |
| 124 | +``` |
| 125 | + |
| 126 | +`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). |
| 127 | + |
| 128 | +## Channels vs events |
| 129 | + |
| 130 | +Both are inbound surfaces on a `chat.agent`, and an agent can list both: |
| 131 | + |
| 132 | +- [`events`](/webhooks/session-routing) (`chat.event`): the webhook is a signal. Delivered to `onAction`; the agent acts, no reply is sent back. |
| 133 | +- `channels` (`slack`, `chat.channels.custom`): the webhook is a chat frontend. Delivered as a turn to `run()`; the reply is posted back. |
0 commit comments