Skip to content

Slack Agents ​

Agent SDK makes it easy to build agents that work in Slack.

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. Connect sets up the Slack app.

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. All messages in one DM share a session by default. Set continuation: "slack.dmthread/v2" to give each top-level DM its own session and thread.

ts
// bot/channels/slack.ts
import { slackChannel } from "@grok/agent-sdk/channels/slack";

export default slackChannel({
  // Reads TRIAGE_SLACK_BOT_TOKEN and TRIAGE_SLACK_APP_TOKEN, so two bots
  // on one host don't collide.
  envPrefix: "TRIAGE",
});

Post one reply with a status chip ​

By default, Slack shows a status chip under the thread while the turn runs, with idle messages until reasoning or tool activity gives it more to show. When the turn finishes, the chip disappears and one complete reply lands in the thread.

ts
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.

ts
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 as well as 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. Use tone to set the voice, level of detail, formatting, and structure of updates.

ts
// bot/extensions/slack.ts
import slackCommunication from "@grok/agent-sdk/extensions/slack-communication";

export default slackCommunication({
  tone: "Be concise but warm. Use bullets for evidence and end with the next action.",
});
ts
export default slackChannel({
  // `post-update` writes mid-turn progress and the final reply.
  reply: { mode: "agentic" },
});

Show steps as cards or as a checklist ​

The agent calls post-card to show one step of its work, such as "Scan the repo", with an id, a title, a status, and optional details. Calling it again with the same id updates that card, for example from in_progress to complete.

cards sets how Slack draws these steps:

cardsWhat Slack showsUse it when
{ display: "timeline" } (default)A separate card in the thread for each stepOne or two short steps
{ display: "plan" }One checklist for every step in the turnSeveral steps that readers track in one place
ts
export default slackChannel({
  reply: {
    mode: "agentic",
    cards: { display: "plan", title: "Release check" },
  },
});
  • title is optional and can be up to 256 characters.
  • pending only works when display is "plan".

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.

Mentions work in any channel the bot has joined. To listen without a mention, list between 1 and 10 channel IDs that start with C or G, plus optional #name entries for readability. On its own, "*" watches thread replies in every joined channel but never wakes on top-level posts.

ts
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. Add the channels:read scope when you use #name entries. On Cursor hosting, debounceMs has no effect.

Control who can message ​

By default, the agent doesn't reply to people outside your workspace, such as Slack Connect guests. Set blockExternals: false only if those people should be able to use the agent.

Anyone in a thread can continue its session by default. To keep the conversation between the agent and the person who started it, set respondTo: "author".

ts
export default slackChannel({
  policy: { blockExternals: false, respondTo: "author" },
});

Events ​

Mentions and DMs wake the agent automatically. Declare onAppMention or onDirectMessage to filter them. Like onChannelPost, each handler returns {} to start a turn or null to skip the message. This handler only starts a turn for DMs that begin with triage::

ts
onDirectMessage: async (_ctx, message) => {
  if (!message.markdown.startsWith("triage:")) {
    return null;
  }
  return {};
},

Call Slack from deterministic code via ctx ​

A hook can post a fixed acknowledgement before the model turn starts, using ctx.thread.post(), ctx.slack.channelId, and ctx.slack.threadTs, while tools call the Slack Web API with ctx.host.slack.getClient().

ts
onAppMention: async (ctx, message) => {
  if (message.markdown.trim() === "") {
    return null;
  }
  await ctx.thread.post("Looking at this now.");
  return {};
},

Connect ​

bash
agent-sdk login
agent-sdk slack create --dir .
agent-sdk slack doctor --prefix TRIAGE
agent-sdk serve --dir . --dev

After 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. doctor catches missing credentials before you start the agent. The CLI reference covers each slack command.

If your workspace requires admin approval, keep slack create running and open the Request approval link it prints. Installing from Deployed Agents doesn't send that request. After an admin approves, select Retry in the Add Slack wizard there.

Choose the bot scopes ​

A Cursor-managed app requests these bot scopes by default:

app_mentions:read, assistant:write, channels:history, channels:read, chat:write, files:read, files:write, groups:history, groups:read, im:history, im:read, im:write, mpim:history, reactions:read, reactions:write, usergroups:read, users:read, users:read.email

To request a different list, set botScopes in slack-manifest.json at the root of the agent project. The app then requests exactly that list:

json
{
  "botScopes": [
    "app_mentions:read",
    "assistant:write",
    "channels:history",
    "chat:write",
    "groups:history",
    "im:history",
    "reactions:read",
    "pins:read"
  ]
}

Then run agent-sdk slack create again (add --env prod for the production app), and Slack updates and reinstalls the app.

  • The list must include app_mentions:read, assistant:write, channels:history, chat:write, groups:history, im:history, and reactions:read.
  • Cursor rejects scopes in the Slack Marketplace forbidden families: admin.*, identity.*, search:read*, triggers:*, apps_configuration:*, and workflow.steps:execute.

The person who runs slack create grants Cursor three user scopes on their own account so it can create and install the app, and botScopes doesn't change them.

If you already own the Slack app, run agent-sdk slack init --manual --dir . --name "Triage" and follow the bdk-setup-slack skill for installation and tokens.