How Do I Build a Slack Bot Powered by Claude API?
Building a Claude-powered Slack bot takes about 2 hours. You need a Slack app (Bolt.js), an Anthropic API key, and a Node.js server. The bot listens for @mentions or messages in designated channels, forwards the text to Claude API, and posts the reply back to the same thread. With @slack/bolt and the @anthropic-ai/sdk packages, the integration is under 150 lines of TypeScript. This guide walks through Slack app setup, event handling, Claude response generation, conversation threading, slash commands, and rate limiting — with a cost analysis table so you can estimate monthly spend before you ship.
1. Slack App Setup with Bolt.js
Create the Slack App
- Go to api.slack.com/apps and click Create New App → From Scratch.
- Enable Socket Mode (Settings → Socket Mode) and generate an App-Level Token with
connections:writescope — store it asSLACK_APP_TOKEN. - Under OAuth & Permissions, add these Bot Token scopes:
app_mentions:read— receive @mention eventschannels:history— read channel messageschat:write— post messagescommands— handle slash commands
- Under Event Subscriptions → Subscribe to Bot Events, add
app_mentionandmessage.im. - Install the app to your workspace and copy the Bot User OAuth Token (
SLACK_BOT_TOKEN).
Install dependencies
npm init -y
npm install @slack/bolt @anthropic-ai/sdk
npm install -D typescript ts-node @types/node
Environment variables
# .env
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
ANTHROPIC_API_KEY=sk-ant-...
2. Event Handling — Messages and Mentions
Create src/index.ts:
import Bolt, { App } from "@slack/bolt";
import Anthropic from "@anthropic-ai/sdk";
const app = new App({
token: process.env.SLACK_BOT_TOKEN!,
appToken: process.env.SLACK_APP_TOKEN!,
socketMode: true,
});
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
// Handle @mentions in channels
app.event("app_mention", async ({ event, say }) => {
const userText = (event as any).text.replace(/<@[A-Z0-9]+>/g, "").trim();
const reply = await getClaude(userText);
await say({
text: reply,
thread_ts: event.ts, // reply in thread
});
});
// Handle direct messages
app.message(async ({ message, say }) => {
const msg = message as any;
if (msg.subtype) return; // ignore bot messages, edits
const reply = await getClaude(msg.text ?? "");
await say({ text: reply, thread_ts: msg.ts });
});
(async () => {
await app.start();
console.log("Bot is running in Socket Mode");
})();
3. Claude API Integration
The getClaude helper calls Claude with a system prompt and returns the text response. Apply prompt caching on the system prompt to cut repeat costs by up to 90%.
async function getClaude(userMessage: string): Promise<string> {
const response = await anthropic.messages.create({
model: "claude-haiku-4-5", // cheapest for chat; swap to sonnet for complex tasks
max_tokens: 1024,
system: [
{
type: "text",
text: `You are a helpful Slack assistant. Be concise — Slack messages should be short.
Reply using Slack mrkdwn formatting (not Markdown). Use *bold*, _italic_, and \`code\` where helpful.`,
cache_control: { type: "ephemeral" }, // prompt caching — pays off after ~2 messages
},
],
messages: [{ role: "user", content: userMessage }],
});
const block = response.content[0];
return block.type === "text" ? block.text : "(no response)";
}
For model selection guidance see Claude Haiku vs Sonnet vs Opus — which model to use.
4. Conversation Threading
To give Claude memory of the thread, fetch prior messages from Slack and pass them as the messages array:
import { WebClient } from "@slack/web-api";
const web = new WebClient(process.env.SLACK_BOT_TOKEN!);
async function getThreadHistory(
channel: string,
threadTs: string
): Promise<Anthropic.MessageParam[]> {
const result = await web.conversations.replies({
channel,
ts: threadTs,
limit: 20, // keep last 20 turns to control token spend
});
const messages: Anthropic.MessageParam[] = [];
for (const msg of result.messages ?? []) {
if (!msg.text || msg.subtype) continue;
const isBotMessage = !!msg.bot_id;
messages.push({
role: isBotMessage ? "assistant" : "user",
content: msg.text,
});
}
return messages;
}
async function getClaudeWithHistory(
channel: string,
threadTs: string,
newMessage: string
): Promise<string> {
const history = await getThreadHistory(channel, threadTs);
history.push({ role: "user", content: newMessage });
const response = await anthropic.messages.create({
model: "claude-haiku-4-5",
max_tokens: 1024,
system: [
{
type: "text",
text: "You are a helpful Slack assistant. Be concise and use Slack mrkdwn formatting.",
cache_control: { type: "ephemeral" },
},
],
messages: history,
});
const block = response.content[0];
return block.type === "text" ? block.text : "(no response)";
}
Update the app_mention handler to call getClaudeWithHistory instead of getClaude.
For advanced multi-turn agent patterns, see the Claude Agent SDK Guide.
5. Slash Commands
Register a /ask command in your Slack app dashboard (Slash Commands → Create New Command, Request URL can be your ngrok or production URL, or leave blank for Socket Mode).
app.command("/ask", async ({ command, ack, respond }) => {
await ack(); // acknowledge within 3 seconds
const userInput = command.text.trim();
if (!userInput) {
await respond({ text: "Usage: `/ask <your question>`", response_type: "ephemeral" });
return;
}
// Show a thinking indicator
await respond({ text: ":hourglass: Asking Claude…", response_type: "in_channel" });
const reply = await getClaude(userInput);
await respond({ text: reply, response_type: "in_channel" });
});
Want production-ready Slack bot patterns, multi-agent pipelines, and tool use recipes? The Claude Agent SDK Cookbook ($49) includes a full Slack bot starter with threading, tool calls, and cost dashboards — copy-paste ready TypeScript.
6. Rate Limiting and Cost Control
Uncontrolled bots can burn API budget fast. Apply two layers of protection:
Token budget per response
const MAX_INPUT_TOKENS = 4096; // guard against huge thread histories
async function getClaudeSafe(messages: Anthropic.MessageParam[]): Promise<string> {
// Roughly estimate tokens: 1 token ≈ 4 chars
const approxTokens = messages.reduce((sum, m) => {
const text = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
return sum + Math.ceil(text.length / 4);
}, 0);
if (approxTokens > MAX_INPUT_TOKENS) {
// Trim oldest messages, keep system context
messages = messages.slice(-10);
}
const response = await anthropic.messages.create({
model: "claude-haiku-4-5",
max_tokens: 512, // keep responses short in Slack
messages,
});
const block = response.content[0];
return block.type === "text" ? block.text : "(no response)";
}
Per-user rate limiting with a sliding window
const userCooldowns = new Map<string, number>();
const COOLDOWN_MS = 5000; // 5 seconds between requests per user
function isRateLimited(userId: string): boolean {
const last = userCooldowns.get(userId) ?? 0;
const now = Date.now();
if (now - last < COOLDOWN_MS) return true;
userCooldowns.set(userId, now);
return false;
}
// Use inside event handlers:
// if (isRateLimited(event.user)) { await say("Please wait a moment."); return; }
Cost Analysis: Monthly Spend by Team Size
Assumes Claude Haiku 4.5 pricing ($0.80/M input, $4.00/M output), average 300 input tokens and 200 output tokens per exchange, prompt caching enabled (90% cache hit saves ~72% of input cost).
| Team Size | Daily Messages | Monthly Messages | Est. Monthly Cost |
|---|---|---|---|
| Small team (10 users) | 50 | 1,500 | ~$0.40 |
| Mid-size (50 users) | 250 | 7,500 | ~$2.00 |
| Large team (200 users) | 1,000 | 30,000 | ~$8.00 |
| Enterprise (500 users) | 2,500 | 75,000 | ~$20.00 |
| Power users (high volume) | 5,000 | 150,000 | ~$40.00 |
Switching to Claude Sonnet (~5x more expensive per token) only makes sense for tasks needing deep reasoning. For conversational Slack bots, Haiku delivers excellent results at a fraction of the cost. See Claude API Cost and Prompt Caching Break-Even for a full cost calculator.
Full Working Example
// src/index.ts — complete minimal bot
import { App } from "@slack/bolt";
import Anthropic from "@anthropic-ai/sdk";
import { WebClient } from "@slack/web-api";
const app = new App({
token: process.env.SLACK_BOT_TOKEN!,
appToken: process.env.SLACK_APP_TOKEN!,
socketMode: true,
});
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const web = new WebClient(process.env.SLACK_BOT_TOKEN!);
const userCooldowns = new Map<string, number>();
function isRateLimited(userId: string): boolean {
const last = userCooldowns.get(userId) ?? 0;
if (Date.now() - last < 5000) return true;
userCooldowns.set(userId, Date.now());
return false;
}
async function ask(text: string): Promise<string> {
const res = await anthropic.messages.create({
model: "claude-haiku-4-5",
max_tokens: 512,
system: [
{
type: "text",
text: "You are a helpful Slack assistant. Be concise. Use Slack mrkdwn formatting.",
cache_control: { type: "ephemeral" },
},
],
messages: [{ role: "user", content: text }],
});
const block = res.content[0];
return block.type === "text" ? block.text : "(no response)";
}
app.event("app_mention", async ({ event, say }) => {
const ev = event as any;
if (isRateLimited(ev.user)) return;
const text = ev.text.replace(/<@[A-Z0-9]+>/g, "").trim();
const reply = await ask(text);
await say({ text: reply, thread_ts: ev.ts });
});
app.command("/ask", async ({ command, ack, respond }) => {
await ack();
if (!command.text.trim()) {
await respond({ text: "Usage: `/ask <question>`", response_type: "ephemeral" });
return;
}
const reply = await ask(command.text.trim());
await respond({ text: reply, response_type: "in_channel" });
});
(async () => {
await app.start();
console.log("Claude Slack bot started");
})();
Run with npx ts-node src/index.ts or compile and run with tsc && node dist/index.js.
Ready to ship a production Slack bot? The Claude Agent SDK Cookbook ($49) includes battle-tested patterns for Slack bots with tool use, database lookups, streaming responses, and multi-workspace support — all in TypeScript with tests included.
Frequently Asked Questions
Can I use Claude with Slack's HTTP mode instead of Socket Mode?
Yes. Replace socketMode: true and appToken with a public HTTPS URL as the Request URL in your Slack app settings. Bolt.js handles both modes with the same event handlers. Socket Mode is easier for local development since it avoids exposing a public endpoint.
How do I prevent the bot from responding to its own messages?
Bolt.js automatically filters out bot messages for message events. For app_mention, check if (event.bot_id) return; at the start of your handler. Also check msg.subtype === "bot_message" in message listeners.
What model should I use for a Slack bot — Haiku or Sonnet?
Use Claude Haiku for general Q&A, summaries, and simple tasks (the vast majority of Slack bot usage). Switch to Sonnet for code review, complex analysis, or anything requiring multi-step reasoning. You can route dynamically: check if the message contains code blocks or technical keywords and select the model accordingly. See Claude Haiku vs Sonnet vs Opus for the full comparison.
How do I give the bot access to company data (e.g., Notion, Jira)?
Use Claude's tool use feature (function calling). Define tools that query your internal APIs, then pass tools to anthropic.messages.create. Claude will call the tool when needed, you execute it and return the result, and Claude incorporates the data in its reply. The Claude Agent SDK Guide covers tool use patterns in detail.
How do I deploy the bot to production?
For Socket Mode bots, any Node.js hosting works: Fly.io, Railway, Render, or a plain VPS. The bot maintains a WebSocket connection to Slack — no public ingress needed. Set your environment variables (SLACK_BOT_TOKEN, SLACK_APP_TOKEN, ANTHROPIC_API_KEY) in your hosting platform's secrets manager. For high-availability, run at least two instances and let Slack distribute events.