showcase/shell-docs/src/content/reference/bot/slack/index.mdx
slack(opts) returns a SlackAdapter — the Slack implementation of the engine's PlatformAdapter boundary. It handles ingress via Bolt (Socket Mode by default), renders the JSX vocabulary to Block Kit within Slack's per-element budgets, streams agent replies via throttled chat.update, and decodes interactions to opaque action ids. @copilotkit/bot-slack is the only package in the stack that talks to Slack.
For app creation, tokens, and first run, see the Slack quickstart.
import { slack } from "@copilotkit/bot-slack";
function slack(opts: SlackAdapterOptions): SlackAdapter;
A SlackAdapter to pass into createBot({ adapters }). It advertises platform: "slack", ackDeadlineMs: 3000, and:
capabilities: {
supportsStreaming: true,
supportsModals: false,
supportsTyping: false,
supportsReactions: false,
maxBlocksPerMessage: 50,
}
The capability-gated Thread methods are all backed: getMessages() via conversations.replies, lookupUser(query) via directory search, and postFile(...) via files.uploadV2. Inbound file uploads are downloaded and delivered to the agent as multimodal content parts.
import { createBot } from "@copilotkit/bot";
import { slack, defaultSlackTools, defaultSlackContext } from "@copilotkit/bot-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],
context: [...defaultSlackContext, ...appContext],
});
The Slack listener pre-filters events to the turns the bot should answer — @-mentions, replies in threads it owns, and DMs — so a single onMention handler usually covers everything. Conversation history is rebuilt from Slack (conversations.replies / conversations.history) on every turn: Slack is the source of truth, so bot restarts don't lose conversations.
thread.runAgent() and thread.stream(...) post a placeholder and edit it in place as text arrives: chat.update calls are queued per message with a minimum gap between flushes (default 800ms); long replies roll over into follow-up messages at a soft 3500-character limit (under Slack's ~4000), breaking at the last newline or space and keeping fenced code blocks whole; dangling markdown (an unclosed fence or bold span) is auto-closed on each flush so the in-flight message always renders. Text is translated per chunk by markdownToMrkdwn.
Every block_actions click is acked within Slack's 3-second deadline (ackDeadlineMs: 3000), then handled asynchronously. decodeInteraction extracts the opaque minted id (ck:…), the control's value, and the message ref — those are the only things that ride in the Slack payload; handler code, other props, and bind() args stay server-side. Unrelated clicks decode to events the bot harmlessly ignores; clicks on actions lost to a restart (with the default in-memory ActionStore) are acked but ignored.
The underlying Bolt App is constructed with deferInitialization — construction is side-effect-free, and bot.start() owns initialization (Bolt init(), then an awaited auth.test to resolve the bot's own user id), so auth and config errors surface to the caller instead of firing in the background.
ActionStore — in-memory only; actions expire on restart