← Back to Payloads
Tutorial2026-06-16

Ship a Slack Bot That Calls Your Agent in 30 Minutes (Bolt + Anthropic)

Bolt for Python plus Anthropic SDK plus Socket Mode — no public URL, no ngrok, no OAuth dance. Your agent runs in Slack threads in under 100 lines.
Quick Access
Install command
$ mrt install tutorial
Browse related skills
Ship a Slack Bot That Calls Your Agent in 30 Minutes (Bolt + Anthropic)

Ship a Slack Bot That Calls Your Agent in 30 Minutes (Bolt + Anthropic)

Most Slack bot tutorials bury you in OAuth flows and ngrok tunnels. You do not need any of it. Socket Mode lets Bolt talk to Slack over an outbound WebSocket. Pair it with the Anthropic SDK and you have a working agent-in-Slack in one file, no exposed port.

Table of contents

  • The Setup (~2 min)
  • The Bot (~80 lines)
  • Why This Pattern
  • Gotchas

Hey guys, Mr. Technology here.

The Setup (~2 min)

Enable Socket Mode on your Slack app with chat:write, app_mentions:read, im:history scopes. Copy the App-Level Token (xapp-...) and Bot Token (xoxb-...) from Basic Information.

bash pip install slack-bolt anthropic

bash export SLACK_APP_TOKEN=xapp-... export SLACK_BOT_TOKEN=xoxb-... export ANTHROPIC_API_KEY=sk-ant-...

The Bot (~80 lines)

Save as bot.py. Threads map cleanly onto Claude conversation turns.

```python import os, anthropic from slack_bolt import App from slack_bolt.adapter.socket_mode import SocketModeHandler

app = App(token=os.environ["SLACK_BOT_TOKEN"]) client = anthropic.Anthropic() SYSTEM = "You are a concise assistant for the engineering team. " \ "Answer in 2-4 sentences unless asked for code."

def history_to_messages(thread: list[dict]) -> list[dict]: bot_id = app.client.auth_test().data["user_id"] msgs = [] for m in thread: if m.get("bot_id") or m.get("subtype") == "bot_message": continue role = "assistant" if m.get("user") == bot_id else "user" msgs.append({"role": role, "content": m["text"]}) return msgs[-10:]

@app.event("app_mention") def on_mention(event, say, client): thread_ts = event.get("thread_ts") or event["ts"] replies = client.conversations_replies( channel=event["channel"], ts=thread_ts, limit=20 )["messages"] msgs = history_to_messages(replies) resp = client.messages.create( model="claude-sonnet-4-5", max_tokens=512, system=SYSTEM, messages=msgs, ) say(text=resp.content[0].text, thread_ts=thread_ts)

@app.event("message") def on_dm(event, say, client): if event.get("channel_type") != "im": return msgs = history_to_messages([event]) msgs.insert(0, {"role": "user", "content": event["text"]}) resp = client.messages.create( model="claude-sonnet-4-5", max_tokens=512, system=SYSTEM, messages=msgs, ) say(text=resp.content[0].text, channel=event["channel"])

if __name__ == "__main__": SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"]).start() ```

bash python bot.py

DM the bot or @-mention it in a channel. It replies in the thread.

Why This Pattern

Socket Mode means zero surface area. No public URL, no ingress. Your laptop opens an outbound WebSocket and Slack streams events to it — the right default for internal tools.

Thread = conversation. conversations_replies returns parent plus replies; feed the last N turns to the model. Memory is free. Cap with msgs[-10:] for cost.

Two handlers, one model. app_mention for channels, message filtered to channel_type == "im" for DMs. Do not over-engineer until you have a second use case.

Gotchas

Bot echo loops. Skipping bot_id is mandatory — without it the bot replies to itself and your token bill explodes in four turns.

Rate limits. ~1 msg/sec per channel. For long answers post a placeholder, then chat.update when the model returns. say() returns the timestamp.

Context growth. limit=20 × 4 KB = 80 KB per call. Trim or summarize older turns with a cheaper Haiku pass.

Streaming is awkward. Bolt does not stream edits natively. Post the full response as one block — users prefer one notification over ten partials.

Permissions. Bot only sees channels it has been invited to. New install means /invite @YourBot everywhere.

Variations

Add tools=[{"name": "lookup_pr", ...}] to messages.create, dispatch tool_use, return tool_result next turn — you have a Slack-resident agent. Deploy the same file unchanged on Fly.io or EC2.

The Take

Bolt Socket Mode is the fastest path from "I have an agent" to "my team is using it." One file, two handlers, zero infra.

Mr. Technology


*Tested June 2026 with slack-bolt 1.18+ and the anthropic Python SDK on claude-sonnet-4-5. Socket Mode requires a Slack workspace where you can install custom apps — free tier works.*

Sources:

Related Dispatches