Skip to navigation

Slack

Bridge Slack threads and Band rooms with SlackAdapter

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. To connect another app, see Build an App Integration.

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:

SourceTriggerWhat the inner adapter gets
SlackAn app_mention in a channel, or a message in a DM with the botThe Slack message, the thread’s earlier messages, and Band tools plus slack_send_message
BandA room message that @mentions the agentThe 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:

ToolPosts toUse it for
slack_send_messageThe bound Slack thread only, not the Band roomThe answer to the person in Slack
band_send_messageThe Band room only, with at least one @mentionAsking 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
  • A Band account at 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

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

Keep agent_config.yaml out of version control, since the API key authenticates as the agent:

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

1

Create the App from the Manifest

Copy the Slack app manifest 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, 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.

2

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
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
ANTHROPIC_API_KEY=sk-ant-...
3

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

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

1

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.

2

Open the Room in Band

In Band, 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.

3

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

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)
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:

ParameterDefaultEffect
innerRequiredThe framework adapter that does the reasoning. Passed to SlackAdapter, not to the config.
appsRequiredOne 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_progressTrueShow tool calls as Slack plan blocks
mirror_slack_contextTrueCopy 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. For every parameter, see SlackAdapter in the SDK Reference.

Limits and Errors

ConditionResult
HTTP transport and a SlackApp without signing_secretValidationError from SlackAdapterConfig: transport='http' requires signing_secret and bot_token on every SlackApp; missing for: <slugs>
Socket Mode and a SlackApp without app_tokenValidationError from SlackAdapterConfig: transport='socket' requires app_token and bot_token on every SlackApp; missing for: <slugs>
apps is emptyValidationError from SlackAdapterConfig: Tuple should have at least 1 item after validation, not 0
Two apps with the same slugValidationError 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 offHTTP 401 invalid signature
HTTP request with a body that is not JSONHTTP 400 invalid JSON
An event ID seen beforeAcknowledged and dropped. The adapter remembers the last 10,000 event IDs.
A channel message without an @mention of the botIgnored
Messages from bots, including this oneIgnored
assistant_thread_startedAcknowledged and ignored
The thread can’t be read, for example not_in_channelThe turn runs with no earlier messages
A long threadOnly the first page that Slack’s conversations.replies returns is read
More tool calls than fit in one Slack messageThe plan block stops at Slack’s 50-block limit and summarizes the rest as …and N more

Next Steps