How to Build a Slack Bot with Claude
Building a Slack bot with Claude means connecting Slack's Events API or Bolt SDK to a Claude-powered backend that reads messages, generates a response, and posts it back to the channel. The core pieces are: a Slack app with the right scopes and event subscriptions, a small server that receives Slack events, a call to Claude to generate the reply, and a way to post that reply back through Slack's Web API.
This guide walks through the whole flow — from creating the Slack app to handling threads, streaming responses, and keeping your bot responsive under Slack's tight timeout windows.
What you need before starting
- A Slack workspace where you can install apps (or a sandbox workspace for testing)
- A Node.js (or Python) server that's publicly reachable, even if just via a tunnel like ngrok during development
- Claude API access — either directly through Anthropic or through a proxy like SubToAPI if you want a plain HTTPS endpoint with your own API keys instead of managing Anthropic credentials and usage tracking yourself
- Basic familiarity with Slack's Bolt framework, which handles signature verification and event routing for you
Step 1: Create the Slack app
Go to api.slack.com/apps and create a new app "from scratch." You'll need:
- Bot token scopes:
chat:write,app_mentions:read, andchannels:history(orim:historyfor DMs) depending on where the bot should respond. - Event subscriptions: subscribe to
app_mentionif you only want the bot to respond when tagged, ormessage.channelsif you want it listening to everything in channels it's invited to. - Socket Mode or HTTP events: Socket Mode avoids exposing a public URL during development, which is the fastest way to iterate locally.
Install the app to your workspace and grab the Bot User OAuth Token and Signing Secret — you'll need both.
Step 2: Set up the Bolt server
Bolt handles signature verification, retries, and event parsing so you don't have to write that boilerplate yourself.
import { App } from "@slack/bolt";
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
socketMode: true,
appToken: process.env.SLACK_APP_TOKEN,
});
app.event("app_mention", async ({ event, say }) => {
const reply = await askClaude(event.text, event.thread_ts || event.ts);
await say({ text: reply, thread_ts: event.thread_ts || event.ts });
});
app.start();
Note the say call posts into the same thread the mention came from — this keeps conversations organized instead of flooding the main channel.
Step 3: Call Claude for the response
This is the function that turns the Slack message into a Claude request. If you're using SubToAPI, the request is a plain HTTPS call with your sub_live_ key — no SDK installation or Anthropic account setup required:
async function askClaude(userText, threadId) {
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SUBTOAPI_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 500,
messages: [
{ role: "user", content: userText }
],
}),
});
const data = await response.json();
return data.content[0].text;
}
Check the quickstart and messages endpoint docs for the full request and response shape, including how usage metadata is returned so you can track cost per Slack workspace or per team if you're running the bot for multiple customers.
Step 4: Handle threads and conversation history
Slack threads map naturally to Claude's messages array. Instead of sending only the latest message, pull the thread's history with conversations.replies and pass it as alternating user/assistant turns:
const history = await app.client.conversations.replies({
channel: event.channel,
ts: event.thread_ts || event.ts,
});
const messages = history.messages.map((m) => ({
role: m.bot_id ? "assistant" : "user",
content: m.text,
}));
This keeps the bot aware of earlier context in a thread without you building your own session store — Slack is already storing the conversation for you.
Step 5: Stream responses to avoid timeouts
Slack's Events API expects an acknowledgment within 3 seconds, but a full Claude response can take longer, especially with longer prompts or larger max_tokens. The standard pattern is to acknowledge the event immediately, then post an initial "thinking" message you edit as tokens arrive:
const placeholder = await say({ text: "Thinking…", thread_ts: threadTs });
// stream from Claude, then update:
await app.client.chat.update({
channel: event.channel,
ts: placeholder.ts,
text: finalText,
});
SubToAPI supports streaming responses over server-sent events — see the streaming docs for the exact event format if you want token-by-token updates instead of a single edit at the end.
Step 6: Add tool use for real actions
A bot that only chats is limited. If you want it to look up a ticket, query an internal API, or post a follow-up task, Claude's tool use lets you define functions the model can call during its response, and you execute them server-side before returning the final answer to Slack. The tools documentation covers the request format for defining tool schemas and handling the tool-call turn.
Step 7: Deploy and manage access
Once it works locally, deploy the server to any host that can run a long-lived Node process (Socket Mode keeps a persistent connection, so serverless functions need HTTP mode instead). For managing Claude access across a team building or maintaining the bot, SubToAPI's pricing plans include per-seat API keys — useful if multiple engineers need their own key for testing without sharing one production credential, and you can see usage broken down by key in the dashboard.
Start with a free trial to generate a key and test the full Slack flow end-to-end before going to production.
questions
Does the bot need to use Socket Mode or HTTP events? Socket Mode is easier for local development because it doesn't require a public URL, but production deployments often use HTTP events behind a load balancer for easier scaling and observability.
How do I keep Claude's responses within Slack's message length limits? Slack truncates messages around 40,000 characters, but for readability keep max_tokens modest (300–800) and split longer answers into a thread reply rather than one giant block.
Can the same bot work in DMs and channels? Yes — subscribe to both app_mention and message.im events, and branch your handler logic based on the channel type (event.channel_type === "im").