# @copilotkit/channels-slack The **Slack `PlatformAdapter`** for [`@copilotkit/channels`](../channels). It connects a Slack workspace to any AG-UI agent: ingress via Bolt (Socket Mode), egress as Block Kit rendered from the `@copilotkit/channels-ui` JSX vocabulary, plus text streaming, opaque-id interactions, and HITL. You write your UI as JSX once (`@copilotkit/channels-ui`) and drive the bot with `@copilotkit/channels`; this package is the only one that talks to Slack. ## Install ```sh pnpm add @copilotkit/channels-slack @copilotkit/channels @copilotkit/channels-ui ``` ## Quickstart ```ts import { createBot } from "@copilotkit/channels"; import { slack, defaultSlackTools, defaultSlackContext, } from "@copilotkit/channels-slack"; const bot = createBot({ adapters: [ slack({ botToken: process.env.SLACK_BOT_TOKEN!, // xoxb-… appToken: process.env.SLACK_APP_TOKEN!, // xapp-… (Socket Mode) }), ], agent: (threadId) => makeAgent(threadId), tools: [...defaultSlackTools, ...appTools], // lookup_slack_user + your tools context: [...defaultSlackContext, ...appContext], // tagging/mrkdwn/thread guidance }); bot.onMention(({ thread }) => thread.runAgent()); await bot.start(); ``` `slack(opts)` returns a `SlackAdapter`. By default it runs in **Socket Mode** (`socketMode: true`) — outbound WebSocket only, no public URL needed. HTTP mode (`socketMode: false`) needs `signingSecret` and a `port`. The Slack listener pre-filters ingress to the turns the bot should answer. By default, DMs are conversational, app mentions respond in-thread, and plain replies in channel/private-channel threads require another app mention. ### Required env | Var | Token | Purpose | | ----------------- | ------- | -------------------------------- | | `SLACK_BOT_TOKEN` | `xoxb-` | Bot token for the Web API. | | `SLACK_APP_TOKEN` | `xapp-` | App-level token for Socket Mode. | ## Response routing Use `respondTo` to choose which Slack message events become `onMention` turns: | Surface | Default behavior | Option | | --------------------------------------- | ----------------------- | ----------------------------------------------------- | | Direct messages (`message.im`) | Respond | `respondTo.directMessages` | | App mentions (`app_mention`) | Respond in-thread | `respondTo.appMentions` / `appMentions.reply` | | Plain channel/private-channel replies | Ignore unless mentioned | `respondTo.threadReplies: "afterBotReply"` for legacy | | Assistant pane | Separate default-on API | `assistant`; not controlled by `respondTo` | | Slash commands, reactions, interactions | Explicit trigger paths | Not controlled by `respondTo` | ```ts // Default routing made explicit. slack({ botToken, appToken, respondTo: { directMessages: true, appMentions: { reply: "thread" }, threadReplies: "mentionsOnly", }, }); ``` ```ts // Legacy owned-thread continuation. slack({ botToken, appToken, respondTo: { threadReplies: "afterBotReply", }, }); ``` For the default mention-only thread behavior, subscribe to `app_mention` and `message.im` events. Add `message.channels` and `message.groups` only when you enable `respondTo.threadReplies: "afterBotReply"` and want Slack to deliver plain channel/private-channel thread replies. ## What it provides ### JSX → Block Kit rendering `renderSlackMessage(ir)` / `renderBlockKit(ir)` translate the `@copilotkit/channels-ui` vocabulary to Block Kit: `Message → blocks`, `Header → header`, `Section → section (mrkdwn)`, `Markdown → markdownToMrkdwn`, `Field(s) → section.fields`, `Context → context`, `Actions → actions`, `Button → button (action_id = minted opaque id)`, `Select → static_select`, `Input → plain_text_input`, `Image → image`, `Divider → divider`. ### Per-element budget Slack caps every element. The renderer degrades by truncate-with-overflow / clamp — it never silently drops content. Limits live in `SLACK_LIMITS`: | Limit | Value | Element | | ------------------ | ----- | -------------------------- | | `blocksPerMessage` | 50 | blocks per message | | `sectionText` | 3000 | section body chars | | `headerText` | 150 | header chars | | `fieldsPerSection` | 10 | fields per section | | `fieldText` | 2000 | field chars | | `actionsElements` | 25 | controls per actions row | | `contextElements` | 10 | elements per context block | | `buttonText` | 75 | button label chars | | `actionId` | 255 | `action_id` chars | | `buttonValue` | 2000 | button value chars | | `selectOptions` | 100 | options per select | ### Colored cards `` renders as a Slack attachment with a colored left bar (Block Kit blocks have no native accent, so accented messages are posted as `attachments: [{ color, blocks }]`). ### Streaming By default, replies stream via Slack's **native streaming API** (`chat.startStream` / `appendStream` / `stopStream`) wherever the reply target is a thread — a true streaming UI rendering **raw markdown** (so real tables and fenced code render natively). A whole turn streams into **one** message: text from every step accumulates into a single bubble (Slack documents only a 12k char limit _per append_, with no cumulative cap, so there is no multi-message splitting), and tool calls surface as native in-message **`task_update`** chunks (a "timeline" of `Using …` → `Used …` steps) instead of separate status messages. Workspaces where structured chunks aren't available degrade automatically to `:wrench:` status rows. Flat DMs (no thread) and any workspace where the streaming API is unavailable fall back automatically to the shipped `chat.update` transport (throttled edits, multi-message chunking, mid-stream bracket auto-close, Markdown → mrkdwn translation). Pass `streaming: "legacy"` to force the `chat.update` transport everywhere. The fallback is transparent — **opting in can never break a bot**: the first `startStream` failure marks the workspace legacy and redoes the stream the old way. ### Feedback buttons (opt-in) Pass `feedback` to attach Slack's native AI feedback row (👍/👎, `context_actions` + `feedback_buttons`) to each finalized streamed reply. Clicks are routed straight to your handler — they never reach the engine's interaction dispatch. Without `feedback`, no buttons are shown. ```ts slack({ botToken, appToken, feedback: { onFeedback: ({ sentiment, user, channel, messageTs }) => { recordFeedback({ sentiment, user, channel, messageTs }); // your telemetry }, // positiveLabel / negativeLabel are optional }, }); ``` The row is attached at `chat.stopStream` (the only streaming call that accepts `blocks`), so it appears on the native path only — the legacy `chat.update` fallback omits it. ### Native "is thinking…" status (everywhere) While the agent runs, the bot shows Slack's **native** loading status (`assistant.threads.setStatus`: "is thinking…") on every thread-anchored reply — channel @-mentions, threads it owns, DMs, and the assistant pane. Slack now accepts this method with the ordinary **`chat:write`** scope (no `assistant:write` needed just for the loading state), so it works for channel-based apps too. The status auto-clears when the reply streams in. Tool progress is surfaced per surface: the pane uses live composer status ("is using \`tool\`…"); elsewhere it uses the native `task_update` timeline (or `:wrench:` rows on older workspaces). Set `assistant: false` to opt out of the status (and pane) entirely. ### Assistant pane (agent-native, default-on) When the Slack app has the **Agents & AI Apps** toggle (an `assistant_view` manifest block + the `assistant:write` scope and `assistant_thread_*` events), the adapter activates Slack's assistant pane with **zero config**: - Opening the pane posts a greeting + tappable prompt chips, and each pane conversation is its own thread (replies stay in-thread). - While the agent runs, native composer status is shown (see above), with "is using \`tool\`…" per tool call. - The pane thread is auto-titled from the first message. Customize via the `assistant` option, or set `assistant: false` to disable pane handling entirely. Apps **without** the toggle behave exactly as before — the pane machinery lies dormant. ```ts slack({ botToken, appToken, assistant: { greeting: "Hi! I can triage issues, search docs, and more.", suggestedPrompts: [ { title: "Triage my open issues", message: "Triage my open issues" }, ], }, }); // Dynamic behavior when a user opens the pane (layers on top of the defaults): bot.onThreadStarted(async ({ thread, user }) => { await thread.setSuggestedPrompts(promptsFor(user)); // await thread.setTitle(...) is also available }); ``` ### Interactions (ack-first) Every Slack `block_actions` click is acked immediately (within the **≤3s** deadline, `ackDeadlineMs = 3000`), then `decodeInteraction` extracts the opaque minted id (`ck:…`), any tiny `bind()` value, and the message ref, and hands an `InteractionEvent` to the engine. The token carries only the opaque id — no props or secrets. Unrelated clicks decode to events the bot harmlessly ignores. ### Human-in-the-loop Use `thread.awaitChoice()` to post an interactive message and block until a click resolves it; the resolved value is the clicked control's value. Agent interrupts (`on_interrupt`) are captured by the run renderer and dispatched to your `onInterrupt` handler, which posts a picker; the click resumes the agent via `thread.resume(value)`. ### Sender-profile resolution & file download The adapter resolves each turn's Slack user id to a richer `PlatformUser` (`{ id, name?, email? }`), cached per id. Inbound files can be downloaded and delivered to the agent as multimodal content parts (`buildFileContentParts`); a tool can post a file back out via `thread.postFile(...)`. ### Built-ins - `defaultSlackTools` — ships `lookup_slack_user` so the agent can resolve a name/handle/email to a `<@USERID>` mention. Spread into `tools`. - `defaultSlackContext` — tagging procedure, Markdown-vs-mrkdwn guidance, and the Slack thread/DM conversation model. Spread into `context`. ## Tool context There is no Slack-specific tool context. Tools receive the single shared `BotToolContext` from `@copilotkit/channels` (`{ thread, message?, user?, signal?, platform }`) and reach Slack power only through capability-gated `thread` methods, which this adapter backs: - `thread.getMessages()` — the current thread's messages (via `conversations.replies`), each a `ThreadMessage` (`{ user?, text, ts?, isBot? }`). - `thread.lookupUser(query)` — resolve a name/handle/email to a `PlatformUser`. - `thread.postFile({ bytes, filename, title?, altText? })` — upload a file back into the thread (`files.uploadV2`). This keeps tools portable: define them with `defineBotTool({...})` and they work against any adapter that advertises the same capabilities. ## Running the demo This package is the **library**. A runnable end-to-end demo wiring all of the above against a real workspace lives in [`examples/slack`](../../examples/slack). ## Slash commands The adapter forwards every slash command Slack delivers to the engine, which routes it to the matching `bot.onCommand` handler (and ignores unregistered ones). Register handlers on the engine — see [`@copilotkit/channels`](../channels/README.md): ```ts bot.onCommand({ name: "triage", description: "Summarize the thread and propose issues.", async handler({ thread, text, user }) { await thread.runAgent({ prompt: `Triage: ${text}` }); }, }); ``` **You must also declare each command in the Slack app config** ("Slash Commands" / app manifest) with the same name — Slack won't deliver an unregistered command, even over Socket Mode. Args arrive as free text (`ctx.text`); the optional `options` schema is for surfaces with native structured args (e.g. Discord) and is unused on Slack. The adapter does not implement `registerCommands`, so the engine skips it (Slack matches commands dynamically rather than registering them up front). ## OAuth bot scopes The following bot token scopes are required or relevant depending on the features your app uses: | Scope | Required for | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | `chat:write` | Posting messages, streaming, ephemeral messages (`chat.postEphemeral`), and opening modals (`views.open`) — all share this single scope. | | `reactions:read` | Reading reactions; subscribe to `reaction_added` / `reaction_removed` events in the app manifest to receive them. | | `reactions:write` | Adding or removing reactions via `reactions.add` / `reactions.remove`. | | `assistant:write` | Native streaming `task_update` tool-timeline chunks and the assistant pane. (The "is thinking…" status works with `chat:write` alone.) | | `files:write` | Uploading files via `thread.postFile()`. | | `users:read` | Resolving Slack user profiles (name, email) via `users.info`. | | `users:read.email` | Resolving user email addresses. | | `channels:history` | Reading channel thread messages via `conversations.replies`. | | `groups:history` | Reading private-channel thread messages via `conversations.replies`. | | `im:history` | Reading DM thread messages via `conversations.replies`. | | `mpim:history` | Reading group-DM thread messages via `conversations.replies`. | ### Notes - **Modals** (`views.open`, `view_submission`, `view_closed`): handled via `chat:write` — no additional scope is needed. - **Ephemeral messages** (`chat.postEphemeral`): covered by `chat:write`. - **Reactions** (`reactions:read` / `reactions:write`): these scopes alone are not enough — you must also subscribe to the `reaction_added` and `reaction_removed` events in the Slack app manifest so that Slack delivers the events to your bot. ## What's NOT in v1 - OAuth / multi-workspace install (single bot token only) - Durable (Redis/DB) `ActionStore` — in-memory only; actions expire on restart - Proactive posting (bot replies only to turns it's part of) ## Exports `slack`, `SlackAdapter`, `SlackAdapterOptions`, `SlackAssistantOptions`, `SlackRespondToOptions`; `createRunRenderer`; `decodeInteraction`, `conversationKeyOf`; `renderBlockKit`, `renderSlackMessage`, `SLACK_LIMITS`; `defaultSlackTools`, `lookupSlackUserTool`, `defaultSlackContext` (+ the individual context entries); `markdownToMrkdwn`; and the preserved mechanics (`SlackConversationStore`, `MessageStream`, `ChunkedMessageStream`, `NativeMessageStream`, `attachSlackListener`, `attachAssistant`, `SanitizingHttpAgent`, `buildFileContentParts`, `autoCloseOpenMarkdown`, and supporting types).