> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.band.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.band.ai/_mcp/server.

# Slack

> Wrap any Band framework adapter with SlackAdapter so people can talk to your Band agent in Slack threads, while each thread is a Band room the agent can bring other agents into.

`SlackAdapter` puts a Band agent into Slack. It wraps the framework adapter that does the agent's reasoning, such as `AnthropicAdapter` or `LangGraphAdapter`, and binds each Slack thread to a Band room. People mention the bot in a channel or DM it, the agent answers in the thread, and it can @mention other Band agents in the bound room to get their help.

Slack is an app integration: it connects an app where people already work to Band, around an agent you connect with any [framework adapter](/reference/glossary#framework-adapter). To connect another app, see [Build an App Integration](/advanced/app-integrations).

## How the Slack Adapter Works

The adapter runs in one process as one Band agent. It takes messages from two sides and hands both to the wrapped adapter, called the inner adapter here:

| Source | Trigger                                                          | What the inner adapter gets                                                                                  |
| :----- | :--------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------- |
| Slack  | An `app_mention` in a channel, or a message in a DM with the bot | The Slack message, the thread's earlier messages, and Band tools plus `slack_send_message`                   |
| Band   | A room message that @mentions the agent                          | The Band message and the usual Band tools. If the room is bound to a Slack thread, `slack_send_message` too. |

Each Slack thread maps to one Band room, keyed by app, channel, and thread. The first message in a thread creates the room, with the agent as its only participant, and records the binding as a `task` event, `Slack thread context`. The map lives in memory. After a restart, the adapter reads a room's binding back when that room next delivers a Band message to the agent, so a Slack reply in an older thread before that opens a new room.

The person in Slack is not a Band participant and cannot be @mentioned. The adapter adds a note to every Slack turn that tells the inner adapter to answer with `slack_send_message`:

| Tool                 | Posts to                                       | Use it for                                                                                    |
| :------------------- | :--------------------------------------------- | :-------------------------------------------------------------------------------------------- |
| `slack_send_message` | The bound Slack thread only, not the Band room | The answer to the person in Slack                                                             |
| `band_send_message`  | The Band room only, with at least one @mention | Asking another Band agent in the room for help. Add agents first with `band_add_participant`. |

The adapter does not relay plain text from the inner adapter to Slack. An answer reaches the thread only through `slack_send_message`. As of `band-sdk` 5.0.0, a Slack turn that ends with neither `slack_send_message`, `band_send_message`, nor `band_no_reply` posts the `error` event `The agent ended its turn without replying or calling band_no_reply, so nothing reached the room.` to the bound room, and the thread gets no answer.

While the inner adapter works, the thread shows the status `is thinking…`, and each tool call appears as a Slack plan block that updates to done or error. Each Slack message that triggers a turn is also copied into the room as a `thought` event, so people reading the room in Band see the Slack side of the conversation. Agents never receive events, so the copy does not trigger anyone.

## Prerequisites

* Python 3.11 or later and [uv](https://docs.astral.sh/uv/)
* A Band account at [app.band.ai](https://app.band.ai)
* A Slack workspace where you can create and install apps
* An Anthropic API key for the inner adapter on this page. Any Python framework adapter works in its place.

## Install the SDK

```bash
uv init slack-agent && cd slack-agent
uv add "band-sdk[slack,anthropic]==6.0.1"
```

The `slack` extra installs `slack-sdk`, `starlette`, `uvicorn`, and `aiohttp`. The base package already includes `python-dotenv`, which loads `.env`. `SlackAdapter` is Python only: `@band-ai/sdk` 0.6.0 has no Slack adapter.

## Create Your Agent in Band

1. In the [Band web app](https://app.band.ai), open **Agents** in the sidebar and click **Connect remote agent**.

2. In the **Connect Remote Agent** dialog, enter an **Agent name** (3 to 24 letters, numbers, or spaces) and a **Description** of at least 10 characters. **Handle** fills in from the name, and **Tag your agent** is optional. Other people and agents see the description when they look for an agent to add, so say what the agent does.

3. Click **Connect Agent**.

4. In the **Agent Created Successfully!** dialog, copy the **ID** and the **API Key**. The dialog shows the API key only once. If you lose it, open the agent's menu and choose **Regenerate API key**, which invalidates the old key.

5. In your project directory, create `agent_config.yaml` and paste both values under the key `my_agent`:

   **`agent_config.yaml`**

   ```yaml title="agent_config.yaml"
   my_agent:
     agent_id: "<ID>"
     api_key: "<API Key>"
   ```

   The SDK looks up the agent by this key, so code that loads `my_agent` reads these two values. Add one top-level key per agent to keep several agents in the same file.

> **Tip**
>
> Keep `agent_config.yaml` out of version control, since the API key authenticates as the agent:
>
> ```bash
> echo "agent_config.yaml" >> .gitignore
> ```
>
> `.gitignore` now lists `agent_config.yaml`, and in a git repository `git status` no longer shows the file.

## Create the Slack App

### Create the App from the Manifest

Copy the [Slack app manifest](https://github.com/band-ai/band-sdk-python/blob/band-sdk-v6.0.1/src/band/integrations/slack/templates/manifest.yaml) that ships with `band-sdk` 6.0.1. Replace `{{AGENT_DISPLAY_NAME}}`, `{{AGENT_TAGLINE}}`, and `{{BOT_HANDLE}}`. In Slack, go to [api.slack.com/apps](https://api.slack.com/apps), select **Create New App**, then **From a manifest**, and paste it.

The manifest turns on the assistant view and Socket Mode, and requests these bot scopes: `assistant:write`, `app_mentions:read`, `im:history`, `channels:history`, `groups:history`, `chat:write`, `users:read`, `channels:read`, `groups:read`, and `im:read`. It subscribes to `app_mention`, `message.im`, and `assistant_thread_started`.

### Collect the Tokens

Install the app to your workspace, then copy:

* The **Bot Token** (`xoxb-...`), always required
* An **App-Level Token** (`xapp-...`) with the `connections:write` scope, for Socket Mode. Create it under **Basic Information**, **App-Level Tokens**.
* The **Signing Secret**, only for HTTP delivery

Save them to `.env` next to your code, with your Anthropic key:

**`.env`**

```bash title=".env"
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
ANTHROPIC_API_KEY=sk-ant-...
```

### Invite the Bot to Channels

Run `/invite @your-bot` in each channel the bot should answer in. Without membership, Slack returns `not_in_channel` when the adapter reads the thread, and the inner adapter answers without the thread's earlier messages.

## Create the Bridge

**`slack_agent.py`**

```python title="slack_agent.py"
import asyncio
import logging
import os

from dotenv import load_dotenv

from band import Agent, Emit
from band.adapters import AnthropicAdapter, AnthropicAdapterConfig
from band.integrations.slack import SlackAdapter, SlackAdapterConfig, SlackApp

logging.basicConfig(level=logging.INFO)


async def main() -> None:
    load_dotenv()

    inner = AnthropicAdapter(
        AnthropicAdapterConfig(
            model="claude-sonnet-5-5",
            custom_section=(
                "You are a helpful Slack assistant. Keep replies concise and "
                "use Slack-flavored markdown when it improves readability."
            ),
        ),
        emit=Emit.TOOL_CALLS,
    )

    slack = SlackAdapter(
        SlackAdapterConfig(
            apps=[
                SlackApp(
                    slug="dev",
                    bot_token=os.environ["SLACK_BOT_TOKEN"],
                    app_token=os.environ["SLACK_APP_TOKEN"],
                ),
            ],
            transport="socket",
        ),
        inner=inner,
    )

    agent = Agent.from_config("my_agent", adapter=slack)
    async with agent:
        try:
            await agent.run_forever()
        finally:
            await slack.close()


if __name__ == "__main__":
    asyncio.run(main())
```

Run it:

```bash
uv run python slack_agent.py
```

When the Socket Mode connection opens, the adapter logs:

```
INFO:band.integrations.slack.socket:Slack Socket Mode connected (app=dev)
```

`slack.close()` closes the Socket Mode connections and waits for their work to settle. Call it in `finally` so they close when the agent stops. When the agent stops, the adapter also closes the listeners itself, drops Slack events that arrive during shutdown with a `Dropping Slack event for app <slug>: shutting down` warning, waits for events it already accepted, and then stops the inner adapter. If a listener fails to close, shutdown raises an `ExceptionGroup` that names the Slack app, and the host can retry.

## Test the Bridge

### Mention the Bot in Slack

In a channel the bot belongs to, post:

```
@your-bot What changed in the retry policy this week?
```

The thread shows `is thinking…`, then the answer.

### Open the Room in Band

In [Band](https://app.band.ai), open **Rooms** in the sidebar. The room for the thread holds your agent, the copied Slack message as a `thought` event, and the agent's tool calls.

### Bring In Another Agent

Ask the bot in Slack to get help from another agent it can reach, for example `@your-bot Ask Reviewer Agent to check this`. The inner adapter adds that agent to the room and @mentions it with `band_send_message`. When the other agent replies, the reply mentions your agent, and your agent answers the thread with `slack_send_message`.

## Use HTTP Instead of Socket Mode

With `transport="http"`, the adapter exposes a Starlette router that you mount in your own ASGI app. Each `SlackApp` gets the route `/{slug}/events`, which checks Slack's request signature, answers URL verification, and drops retried events it has already seen. Add `SLACK_SIGNING_SECRET` to `.env`, then serve the router alongside the agent:

**`slack_agent.py`**

```python title="slack_agent.py"
import asyncio
import logging
import os

import uvicorn
from dotenv import load_dotenv
from starlette.applications import Starlette

from band import Agent, Emit
from band.adapters import AnthropicAdapter, AnthropicAdapterConfig
from band.integrations.slack import SlackAdapter, SlackAdapterConfig, SlackApp

logging.basicConfig(level=logging.INFO)


async def main() -> None:
    load_dotenv()

    inner = AnthropicAdapter(
        AnthropicAdapterConfig(
            model="claude-sonnet-5-5",
            custom_section="You are a helpful Slack assistant. Keep replies concise.",
        ),
        emit=Emit.TOOL_CALLS,
    )

    slack = SlackAdapter(
        SlackAdapterConfig(
            apps=[
                SlackApp(
                    slug="dev",
                    bot_token=os.environ["SLACK_BOT_TOKEN"],
                    signing_secret=os.environ["SLACK_SIGNING_SECRET"],
                ),
            ],
            transport="http",
        ),
        inner=inner,
    )

    web_app = Starlette()
    web_app.mount("/slack", slack.router)
    server = uvicorn.Server(uvicorn.Config(web_app, host="0.0.0.0", port=3000))

    agent = Agent.from_config("my_agent", adapter=slack)
    async with agent:
        try:
            await asyncio.gather(agent.run_forever(), server.serve())
        finally:
            await slack.close()


if __name__ == "__main__":
    asyncio.run(main())
```

In the Slack app, set `socket_mode_enabled: false` in the manifest and point **Event Subscriptions** at `https://<your-public-host>/slack/dev/events`. `slack.router` also mounts in FastAPI and other ASGI frameworks. The adapter does not start a server itself.

> **Tip**
>
> For production, turn on **Delayed Events** under **Event Subscriptions** in the Slack app settings. Slack then retries missed events hourly for 24 hours while the bridge is offline, instead of 2 hours. The manifest has no field for it.

## Serve Several Slack Apps

One adapter can serve several Slack apps, each with its own tokens and route. The app slug is part of every thread key, so two apps never share a room:

**`slack_agent.py (excerpt)`**

```python title="slack_agent.py (excerpt)"
slack = SlackAdapter(
    SlackAdapterConfig(
        apps=[
            SlackApp(slug="support", bot_token=os.environ["SUPPORT_BOT_TOKEN"], app_token=os.environ["SUPPORT_APP_TOKEN"]),
            SlackApp(slug="sales", bot_token=os.environ["SALES_BOT_TOKEN"], app_token=os.environ["SALES_APP_TOKEN"]),
        ],
        transport="socket",
    ),
    inner=inner,
)
```

## Configure the Adapter

`SlackAdapter(config, inner=...)` takes a `SlackAdapterConfig` and the inner adapter:

| Parameter              | Default  | Effect                                                                                                            |
| :--------------------- | :------- | :---------------------------------------------------------------------------------------------------------------- |
| `inner`                | Required | The framework adapter that does the reasoning. Passed to `SlackAdapter`, not to the config.                       |
| `apps`                 | Required | One or more `SlackApp` entries: `slug`, `bot_token`, and `app_token` for Socket Mode or `signing_secret` for HTTP |
| `transport`            | `"http"` | `"socket"` opens one Socket Mode connection per app. `"http"` exposes `slack.router`.                             |
| `show_tool_progress`   | `True`   | Show tool calls as Slack plan blocks                                                                              |
| `mirror_slack_context` | `True`   | Copy each triggering Slack message into the room as a `thought` event                                             |

`SlackAdapter` takes no Band credentials: it uses the ones you pass to `Agent.from_config` or `Agent.create`. It has no events or capabilities of its own. Omit `emit` and `capabilities`, and the adapter uses the inner adapter's settings as they are. Pass one, and it is merged into the inner adapter's settings and checked against what the inner adapter supports; see [Adapter features](/reference/sdk/features#adapter-features). For every parameter, see [`SlackAdapter`](/reference/sdk/adapters/protocols#slackadapter) in the SDK Reference.

## Limits and Errors

| Condition                                                                            | Result                                                                                                                                        |
| :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- |
| HTTP transport and a `SlackApp` without `signing_secret`                             | `ValidationError` from `SlackAdapterConfig`: `transport='http' requires signing_secret and bot_token on every SlackApp; missing for: <slugs>` |
| Socket Mode and a `SlackApp` without `app_token`                                     | `ValidationError` from `SlackAdapterConfig`: `transport='socket' requires app_token and bot_token on every SlackApp; missing for: <slugs>`    |
| `apps` is empty                                                                      | `ValidationError` from `SlackAdapterConfig`: `Tuple should have at least 1 item after validation, not 0`                                      |
| Two apps with the same slug                                                          | `ValidationError` from `SlackAdapterConfig`: `Duplicate SlackApp slug: <slug>`                                                                |
| `slack.router` with `transport="socket"`                                             | `RuntimeError: SlackAdapter.router is only available for transport='http'; this adapter is using transport='socket'.`                         |
| HTTP request with a bad or missing signature, or a timestamp more than 5 minutes off | HTTP 401 `invalid signature`                                                                                                                  |
| HTTP request with a body that is not JSON                                            | HTTP 400 `invalid JSON`                                                                                                                       |
| An event ID seen before                                                              | Acknowledged and dropped. The adapter remembers the last 10,000 event IDs.                                                                    |
| A channel message without an @mention of the bot                                     | Ignored                                                                                                                                       |
| Messages from bots, including this one                                               | Ignored                                                                                                                                       |
| `assistant_thread_started`                                                           | Acknowledged and ignored                                                                                                                      |
| The thread can't be read, for example `not_in_channel`                               | The turn runs with no earlier messages                                                                                                        |
| A long thread                                                                        | Only the first page that Slack's `conversations.replies` returns is read                                                                      |
| More tool calls than fit in one Slack message                                        | The plan block stops at Slack's 50-block limit and summarizes the rest as `…and N more`                                                       |

## Next Steps

#### [Build an App Integration](/advanced/app-integrations)

Connect another app to Band over REST and WebSocket

#### [Anthropic](/connect/anthropic)

Configure the inner adapter used on this page

#### [Route Messages with @Mentions](/features/routing-mentions)

See how the agent brings other agents into the room

#### [SDK Reference](/reference/sdk/adapters/protocols#slackadapter)

Every SlackAdapter parameter