Slack agents
A Slack agent wakes when someone mentions it, sends it a DM, or posts in a channel it watches. From there, it keeps the conversation in one thread and can reply all at once, stream the answer live, or decide for itself when to post updates.
Run bdk slack create --dir ., then bdk slack doctor.
Answer a mention or DM
Mention the bot in a channel or send it a DM, and it will answer in a thread on that message. Any follow-up in the same thread continues the conversation where it left off.
// bot/channels/slack.tsimport { slackChannel } from "@cursor/bdk/channels/slack";export default slackChannel({ // TRIAGE_SLACK_BOT_TOKEN and TRIAGE_SLACK_APP_TOKEN envPrefix: "TRIAGE",});Post one reply with a status chip
By default, Slack shows a status chip beneath the thread while the turn runs, cycling through idle messages until reasoning or tool activity gives it something more specific to show. When the turn finishes, the chip disappears and one complete reply lands in the thread.
export default slackChannel({ reply: { mode: "post", status: { // First line of each reasoning block on the chip. reasoning: true, // Tool names as they start ("Running grep, glob..."). tools: true, idle: ["Checking the request...", "Reviewing recent activity..."], }, },});Stream a live reply
In stream mode, the reply appears immediately and grows as the model writes. Slack shows its generic "is working..." state until the stream ends, while optional reasoning and tool cards keep progress inside the same message.
export default slackChannel({ reply: { mode: "stream", // Thinking card inside the streamed message. reasoningCard: true, // One card per tool in that same message. toolCards: true, },});Let the agent own every message
Use agentic mode when the agent should decide how to communicate, not only what to say. The bot reacts to the parent message with eyes and shows a thinking chip, then the agent chooses when to post progress and how to deliver the final reply.
slack-communication gives those posts a Slack-aware voice by default:
lead with the result, share progress when the picture changes, and keep
tool chatter off the thread. Use tone for the rest of the
communication contract, including voice, level of detail, formatting,
and how updates should be structured.
// bot/extensions/slack.tsimport slackCommunication from "@cursor/bdk/extensions/slack-communication";export default slackCommunication({ tone: "Be concise but warm. Use bullets for evidence and end with the next action.",});export default slackChannel({ // `post-update` writes mid-turn progress and the final reply. reply: { mode: "agentic" },});Wake on a channel post
This example watches #triage-alerts, so any substantial top-level
message starts a thread even when nobody mentions the bot. The length
guard filters out short posts before they can start a turn.
Mentions work in any channel the bot has joined. Listening without a
mention requires an explicit allowlist with at least one channel ID
starting with C or G and at most 10 distinct IDs. You can add
#name entries for readability. A lone "*" watches replies in every
joined channel (posts: "replies"), but it does not wake on top-level
posts. Do not mix "*" with channel IDs.
export default slackChannel({ engagement: { channelPosts: { allow: ["C0123ABCDEF", "#triage-alerts"], posts: "top-level", debounceMs: 15_000, }, }, onChannelPost: async (_ctx, message) => { if (message.markdown.length <= 20) { return null; } // `{}` starts a model turn as the sender. return {}; },});When you create the app, pass --channel-posts and invite the bot to
every channel in the allowlist.
On Cursor-managed hosting, the channel IDs in allow define which
top-level posts can wake the agent. Replies in threads the agent has
already joined still arrive. debounceMs applies only when you
self-host. Add the channels:read scope when you include #name
labels.
Control who can message
By default, people outside your workspace get no reply. This matters
most in Slack Connect channels, where external collaborators share the
channel and can mention or reply to bots installed by your
organization. Keeping blockExternals on prevents them from starting
or steering an agent session and, through it, directing any tools
available to the agent.
Set blockExternals: false only when external collaborators are
intended users of the agent.
export default slackChannel({ // Let external collaborators start and continue agent sessions. policy: { blockExternals: false },});Anyone in a thread can continue its session by default. If the
conversation should stay between the agent and its original author,
set respondTo: "author".
export default slackChannel({ policy: { respondTo: "author" },});Events
Mentions and DMs wake the agent automatically, while onChannelPost
runs for messages in watched channels. Declare onAppMention or
onDirectMessage when you need to filter those default wakes; each
handler returns {} to start a turn or null to skip it.
For example, this handler only starts a turn for DMs prefixed with
triage::
onDirectMessage: async (_ctx, message) => { if (!message.markdown.startsWith("triage:")) { return null; } // `{}` starts a model turn as the sender. return {};},Thread affinity
A Slack thread maps to one agent session, so every follow-up in that thread resumes the same conversation.
Call Slack from deterministic code via ctx
Channel hooks can post before starting the model turn, which is useful
for deterministic acknowledgements such as Looking at this now. The
same bot credential also lets tools call Slack later in the turn:
-
Channel hooks:
ctx.thread.post(),ctx.slack.channelId,ctx.slack.threadTs -
Tools:
ctx.host.slack.getClient()
onAppMention: async (ctx, message) => { if (message.markdown.trim() === "") { return null; } await ctx.thread.post("Looking at this now."); return {};},Use the IDs on the hook ctx when handling an event, and
ctx.host.slack.getClient() when a tool needs the Slack Web API.
Connect
bdk loginbdk slack create --dir .bdk slack doctor --prefix TRIAGEAfter you sign in, slack create opens Add Slack to this agent in
the browser and writes the resulting tokens to .env.local without
printing them. You can open the same wizard from Integrations on
Deployed Agents.
If Slack requires workspace-admin approval, keep slack create running and
open the Request approval link printed by the CLI. Installing from the
dashboard doesn't send that request for you. After an admin approves it,
return to the dashboard's Add Slack wizard and select Retry.
If you already own the Slack app, initialize it with
bdk slack init --manual --dir . --name "Triage" and follow the
Slack setup skill for installation
and tokens.
Test locally
Run doctor first to catch missing credentials, then start the agent
and send it the same mention or DM a teammate would.
bdk slack doctor --prefix TRIAGEbdk serve --dir . --devPractices
-
If the tokens are missing, the Slack channel stays idle while the rest of
servestarts normally. -
Keep
channelPosts.allownarrow because each qualifying post can start a model turn. -
Set
envPrefixwhen two Slack bots share a host so the token names do not collide.
Related
- CLI reference:
slack create,doctor,init --manual skills/setup-slack/SKILL.md: the companion skill for coding agents- Webhooks and custom channels: host prep on mention and DM handlers
- Evals: freeze a mention as a regression check