# 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 `agent-sdk slack create --dir .`, then `agent-sdk 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.

```ts
// bot/channels/slack.ts
import { slackChannel } from "@grok/agent-sdk/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.

```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, 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.

```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". Each card has an `id`, a `title`, a `status`, and
optional `details`. When the agent calls `post-card` again with the same
`id`, Slack updates that card. The agent uses this to move a step from
`in_progress` to `complete`.

`cards` sets how Slack draws these steps:

| `cards` | What Slack shows | Use it when |
| --- | --- | --- |
| `{ display: "timeline" }` (default) | Each step is a separate card in the thread. | The turn has one or two short steps. |
| `{ display: "plan" }` | All steps from one turn are in one checklist. | The turn has several steps, and readers want to see progress in one place. |

This bot shows its steps as one checklist with the title "Release check":

```ts
export default slackChannel({
  reply: {
    mode: "agentic",
    cards: { display: "plan", title: "Release check" },
  },
});
```

Know these rules for a plan:

- The `title` is optional. It can have at most 256 characters.
- You set the `title` in the channel configuration. The agent cannot
  change the title.
- Each turn starts a new checklist with the same title.
- The `pending` status shows a step that has not started. Use `pending`
  only in a plan.
- `slackChannel` stops at startup if `cards` is not valid. For example,
  a `title` with `display: "timeline"` stops startup.

`display` is Slack's
[`task_display_mode`](https://docs.slack.dev/reference/methods/chat.startStream/),
and the channel sends `title` as a `plan_update` chunk.

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

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

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.

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

```ts
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:`:

```ts
onDirectMessage: async (_ctx, message) => {
  if (!message.markdown.startsWith("triage:")) {
    return null;
  }
  // `{}` starts a model turn as the sender.
  return {};
},
```

## Thread affinity

Mention the bot in a channel and it answers in a thread on that message.
Follow-ups in the same thread continue that session.

A direct message continues the one session for that DM, including replies
under later top-level messages. To give each top-level DM its own session,
and keep replies in that message's thread on it, set the continuation
selector:

```ts
export default slackChannel({
  continuation: "slack.dmthread/v2",
});
```

Under `slack.dmthread/v2`, a DM thread behaves like a channel thread: a
reply loads the thread's history, the turn edits only its own placeholder,
and an approval click must come from that thread.

Channel threads keep one session per thread with either selector.
`slack.none/v2` starts a new session on every delivery.

## 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()`

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

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

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](https://cursor.com/dashboard/deployed-agents).

If Slack requires workspace-admin approval, keep `slack create` running and
open the **Request approval** link printed by the CLI. Installing from
[Deployed Agents](https://cursor.com/dashboard/deployed-agents) doesn't send that request for you. After an admin approves it, return to the **Add Slack** wizard there and select **Retry**.

### 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). The CLI shows the list before it opens the browser. Slack updates
the app and installs it again. If the workspace requires admin approval,
the approval request lists every scope the app requests.

- The list must include the required scopes: `app_mentions:read`,
  `assistant:write`, `channels:history`, `chat:write`, `groups:history`,
  `im:history`, and `reactions:read`. The Slack channel needs them to
  receive mentions, DMs, channel posts, and reactions, and to reply.
- If you remove a default scope, the features that use it stop working.
  For example, file uploads need `files:write`.
- Use bot scopes only, at most 50. User scopes are not supported.
- Cursor rejects scopes in the Slack Marketplace forbidden families:
  `admin.*`, `identity.*`, `search:read*`, `triggers:*`,
  `apps_configuration:*`, and `workflow.steps:execute`.
- Without `botScopes`, the app requests the default list plus any other
  scopes it already has. To remove a scope you added, list the scopes you
  want and run `agent-sdk slack create` again.

The person who runs `agent-sdk slack create` also grants Cursor three user
scopes in their own Slack account: `app_configurations:read`,
`app_configurations:write`, and `managed_apps:install`. Cursor uses them
to create, update, and install the agent's app. They are not scopes of
the agent's app, and `botScopes` does not change them.

If you already own the Slack app, initialize it with
`agent-sdk slack init --manual --dir . --name "Triage"` and follow the
[Slack setup skill](/docs/package/skills/setup-slack/SKILL.md) 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.

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

## Practices

- If the tokens are missing, the Slack channel stays idle while the
  rest of `serve` starts normally.

- Keep `channelPosts.allow` narrow because each qualifying post can
  start a model turn.

- Set `envPrefix` when two Slack bots share a host so the token names
  do not collide.

## Related

- [CLI reference](/docs/reference/cli.md#slack): `slack create`,
  `doctor`, `init --manual`
- [`skills/setup-slack/SKILL.md`](/docs/package/skills/setup-slack/SKILL.md):
  the companion skill for coding agents
- [Webhooks and custom channels](/docs/guides/webhooks.md): host prep on mention
  and DM handlers
- [Evals](/docs/evals.md): freeze a mention as a regression check
