Slack Adapter

Expose a Band agent as a Slack-native AI app

The SlackAdapter wraps an existing framework adapter (the “brain”, such as AnthropicAdapter or LangGraphAdapter) and bridges it into Slack. Mention the bot in a channel or DM and it replies in the thread, backed by your agent’s reasoning and the Band platform.

Unlike a gateway, the Slack adapter is a wrapper: one process, one Band agent identity. It decorates the inner adapter with Slack ingress and egress and relays messages between Slack threads and Band rooms in both directions.


Prerequisites

Before starting, complete the Setup tutorial:

  • SDK installed
  • Agent created on the platform
  • .env and agent_config.yaml configured

Install the Slack extra plus a brain adapter (Anthropic here):

$uv add "band-sdk[slack,anthropic]"

The slack extra installs slack-sdk, starlette, uvicorn, and aiohttp.


Set Up the Slack App

The bridge expects an installed Slack app with the right scopes and events. A maintained manifest ships with the SDK at src/band/integrations/slack/templates/manifest.yaml.

1

Create the app from the manifest

In Slack, go to Create New App then From a manifest, and paste the bundled manifest.yaml. It declares:

  • AI App (assistant_view + assistant:write) so the assistant pane, status indicators, and Block Kit plan/task blocks render.
  • Thread-context scopes: app_mentions:read, im:history, channels:history, groups:history, chat:write, users:read, plus channels:read/groups:read/im:read to resolve channel names.
  • Events app_mention, message.im, and assistant_thread_started.
  • Socket Mode enabled (no public URL or signing secret needed to start).
2

Install the app and grab tokens

Install the app to your workspace, then collect:

  • Bot Token (xoxb-...), always required.
  • App-Level Token (xapp-...) with the connections:write scope, required for Socket Mode. Generate it under Basic Information then App-Level Tokens.
  • Signing Secret, required only for HTTP transport.
3

Invite the bot to channels

Run /invite @your-bot in any channel you want it to read. channels:history alone does not grant access to channels the bot is not a member of, and conversations.replies returns not_in_channel without it.


Choose a Transport

The adapter supports two transports that share the same downstream pipeline, so status indicators, plan blocks, thread backfill, and session rehydration behave identically:

TransportWhen to useNeeds
Socket ModeEasiest start. Works behind any NAT or firewall, no public URL.app_token (xapp-...) per app
HTTPProduction behind a public host. Mounts into your ASGI app.Public URL + signing_secret per app

The constructor default is transport="http". The quickstart below uses Socket Mode because it needs no public URL.


Quickstart: Socket Mode Bot

Create slack_bot.py. This wraps an Anthropic brain and runs it as a Slack AI app over Socket Mode:

1import asyncio
2import logging
3import os
4from dotenv import load_dotenv
5from band import Agent, AdapterFeatures, Emit
6from band.adapters import AnthropicAdapter
7from band.config import load_agent_config
8from band.integrations.slack import SlackAdapter, SlackApp
9
10logging.basicConfig(level=logging.INFO)
11logger = logging.getLogger(__name__)
12
13async def main():
14 load_dotenv()
15 agent_id, api_key = load_agent_config("slack_basic_bot")
16
17 # The brain. Emit.EXECUTION records tool calls into the Band room timeline.
18 brain = AnthropicAdapter(
19 model="claude-sonnet-4-6",
20 prompt=(
21 "You are a helpful Slack assistant. Keep replies concise and "
22 "use Slack-flavored markdown when it improves readability."
23 ),
24 features=AdapterFeatures(emit={Emit.EXECUTION}),
25 )
26
27 slack = SlackAdapter(
28 inner=brain,
29 apps=[
30 SlackApp(
31 slug="dev",
32 bot_token=os.getenv("SLACK_BOT_TOKEN"),
33 app_token=os.getenv("SLACK_APP_TOKEN"), # xapp-..., Socket Mode
34 ),
35 ],
36 rest_url=os.getenv("BAND_REST_URL"),
37 api_key=api_key,
38 transport="socket",
39 )
40
41 agent = Agent.create(
42 adapter=slack,
43 agent_id=agent_id,
44 api_key=api_key,
45 ws_url=os.getenv("BAND_WS_URL"),
46 rest_url=os.getenv("BAND_REST_URL"),
47 )
48
49 logger.info("Starting Slack bot (Socket Mode)...")
50 async with agent:
51 try:
52 await agent.run_forever()
53 finally:
54 await slack.close()
55
56if __name__ == "__main__":
57 asyncio.run(main())

Set the required environment variables in .env:

$SLACK_BOT_TOKEN=xoxb-...
$SLACK_APP_TOKEN=xapp-... # Socket Mode only
$ANTHROPIC_API_KEY=sk-ant-...
$BAND_REST_URL=...
$BAND_WS_URL=...

Run it:

$uv run python slack_bot.py

Mention the bot in a channel it belongs to, or DM it. It replies in the thread.

The adapter starts its Socket Mode websockets when the agent connects, and slack.close() shuts them down cleanly. Always close it in a finally block.


HTTP Transport

For production, use HTTP transport and mount the adapter’s router into your own ASGI app. Each SlackApp exposes a route at /{slug}/events.

1from fastapi import FastAPI
2
3slack = SlackAdapter(
4 inner=brain,
5 apps=[
6 SlackApp(
7 slug="dev",
8 bot_token=os.getenv("SLACK_BOT_TOKEN"),
9 signing_secret=os.getenv("SLACK_SIGNING_SECRET"), # required for HTTP
10 ),
11 ],
12 rest_url=os.getenv("BAND_REST_URL"),
13 api_key=api_key,
14 transport="http",
15)
16
17app = FastAPI()
18app.mount("/slack", slack.router)

Point your Slack app’s Event Subscriptions request URL at https://<your-public-host>/slack/dev/events. The router verifies Slack’s HMAC signature and handles URL verification and retry idempotency.

slack.router is only available for transport="http". Accessing it in Socket Mode raises RuntimeError.


How It Works

When a Slack user mentions the bot or DMs it:

  1. Thread binding - The adapter maps the Slack channel:thread_ts (scoped by app slug) to a Band room, creating one if needed. A per-thread lock collapses concurrent events onto a single room.
  2. Thread backfill - Each turn refetches conversations.replies so the brain sees the full thread history, including messages it missed while offline.
  3. Reasoning - The Slack event is synthesized into a PlatformMessage and passed to the inner adapter with REST-backed tools.
  4. Reply - The brain’s reply is posted back into the originating Slack thread.

Two outbound tools

When a room is bound to a Slack thread, the brain sees two send tools and picks based on intent:

  • slack_send_message posts a plain-text reply into the bound Slack thread. Use this for the user-facing answer. It does not post to the Band room.
  • band_send_message sends a real Band message to peers in the room (still requires at least one @mention). Use this to coordinate with other Band agents.

Status and progress

The adapter shows a thinking status via assistant.threads.setStatus while the brain works, and renders Block Kit plan and task blocks so tool-call progress is visible in Slack (capped at Slack’s 50-block limit with an overflow summary). Disable with show_tool_progress=False.

Session rehydration and context mirroring

The Slack thread binding is stored in the room’s bootstrap task event, so the bridge recovers thread context after a restart. By default each inbound Slack turn is also mirrored into the bound Band room as a context-only event (tagged slack_mirror) so the Band audit timeline reflects the Slack conversation. These mirrored events never loop back into the brain’s history. Disable with mirror_slack_context=False.


Multiple Slack Apps

One adapter can serve several Slack apps, each with its own bot token and route. Room lookup is keyed by app slug, so two apps sharing a channel:thread_ts tuple map to distinct rooms with no cross-workspace reply routing.

1slack = SlackAdapter(
2 inner=brain,
3 apps=[
4 SlackApp(slug="support", bot_token="xoxb-...", app_token="xapp-..."),
5 SlackApp(slug="sales", bot_token="xoxb-...", app_token="xapp-..."),
6 ],
7 rest_url=os.getenv("BAND_REST_URL"),
8 api_key=api_key,
9 transport="socket",
10)

Configuration Options

SlackAdapter

1slack = SlackAdapter(
2 # The framework adapter that does the reasoning (required)
3 inner=brain,
4
5 # One or more Slack app configurations (required)
6 apps=[SlackApp(...)],
7
8 # Band REST API base URL
9 rest_url="https://app.band.ai",
10
11 # API key for the Band agent (same key passed to Agent.create)
12 api_key="...",
13
14 # "http" (default) mounts a router; "socket" opens a websocket per app
15 transport="http",
16
17 # Render Block Kit plan/task progress blocks in Slack
18 show_tool_progress=True,
19
20 # Mirror inbound Slack turns into the Band room as context-only events
21 mirror_slack_context=True,
22)

SlackApp

1SlackApp(
2 # URL-safe identifier, used as the HTTP route segment /{slug}/events
3 slug="dev",
4
5 # Bot token (xoxb-...) for outbound Slack API calls (always required)
6 bot_token="xoxb-...",
7
8 # Signing secret for HMAC verification (required for HTTP transport)
9 signing_secret="...",
10
11 # App-level token (xapp-...) to open a Socket Mode websocket
12 # (required for Socket Mode)
13 app_token="xapp-...",
14)

The adapter validates token combinations at construction time. Missing a signing_secret for HTTP, or an app_token for Socket Mode, raises ValueError.


Operational Notes

The bot must be a channel member. Run /invite @your-bot in every channel it should read. Without membership, thread backfill fails with not_in_channel and the brain loses context.

Enable “Delayed Events” for production. Under the Slack app’s Event Subscriptions settings, turn on “Delayed Events” so Slack keeps retrying missed events hourly for 24h while the bridge is offline (the default is 2h). This is a GUI toggle with no manifest field, so it must be set manually after the app is created. See the Slack retry events changelog.


Next Steps