Build a Custom Slack AI Bot — 2026 Complete Tutorial
Build a Custom Slack AI Bot — 2026 Complete Tutorial
Why This Matters
Slack remains the dominant workplace communication platform in 2026, and AI bots have become essential productivity tools. Instead of relying on generic bots, building your own custom Slack AI bot lets you tailor responses to your team’s specific needs — integrating company knowledge, custom tools, and preferred AI models.
This tutorial builds a production-ready Slack AI bot using Python, Slack Bolt SDK, and multi-model AI support.
Prerequisites
- Slack workspace with admin access (any plan works)
- Python 3.11+ installed
- Ngrok or public URL for development tunnels
- OpenAI and/or Anthropic API key for AI capabilities
- Basic Python knowledge and Slack API familiarity
Check your setup:
python3 --version
# Expected: 3.11+
ngrok version
# Expected: 3.x
pip3 install slack-bolt openai anthropic python-dotenv
Step-by-Step
Step 1: Set Up the Slack App
Create a Slack app at api.slack.com/apps:
# Create a new app
# Choose "From scratch" → name it "AI Assistant" → select your workspace
Configure bot scopes under OAuth & Permissions:
Bot Token Scopes:
• app_mentions:read — Read when bot is @mentioned
• channels:history — Read message history in channels
• channels:read — View channel information
• chat:write — Send messages as the bot
• files:read — Read file contents
• users:read — View user profile information
• im:history — Read DMs with the bot
• im:write — Send DMs
• reactions:read — Read emoji reactions
• reactions:write — Add emoji reactions
Install the app to your workspace and save the Bot Token (starts with xoxb-) and Signing Secret from Basic Information.
Enable Socket Mode for real-time events:
# Go to Socket Mode → Enable
# Generate an App-Level Token with `connections:write` scope
Step 2: Create the Basic Bot Structure
mkdir slack-ai-bot
cd slack-ai-bot
python3 -m venv .venv
source .venv/bin/activate
pip install slack-bolt openai anthropic python-dotenv
Create the environment file:
# .env
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token
SLACK_SIGNING_SECRET=your-signing-secret
OPENAI_API_KEY=sk-your-openai-key
ANTHROPIC_API_KEY=sk-ant-your-anthropic-key
BOT_USER_ID= # Fill after first run
Basic bot entry point:
# app.py
import os
import logging
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
from dotenv import load_dotenv
load_dotenv()
# Initialize Slack app
app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"],
)
logging.basicConfig(level=logging.INFO)
@app.event("app_mention")
def handle_mention(event, say):
"""Respond when the bot is @mentioned."""
user = event["user"]
text = event.get("text", "")
channel = event["channel"]
# Remove bot mention from text
bot_user_id = os.environ.get("BOT_USER_ID", "")
user_message = text.replace(f"<@{bot_user_id}>", "").strip()
say(f"Hey <@{user}>! I'm thinking about: '{user_message}'")
@app.event("message")
def handle_dm(event, say):
"""Respond to direct messages."""
if event.get("channel_type") == "im" and "subtype" not in event:
user = event["user"]
text = event.get("text", "")
say(f"I received your DM: '{text}'")
if __name__ == "__main__":
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
logging.info("⚡ AI Slack Bot is running!")
handler.start()
Find your bot’s user ID by running the bot and @mentioning it:
# run this as a one-off to get bot ID
@app.event("app_mention")
def get_bot_id(event, say, client):
bot_info = client.auth_test()
print(f"Bot User ID: {bot_info['user_id']}")
# Add to your .env: BOT_USER_ID=Uxxxxx
Step 3: Add AI Response Capabilities
Build the AI handler that chooses between models:
# ai_handler.py
import os
import json
from openai import OpenAI
from anthropic import Anthropic
from typing import Optional
class AIHandler:
def __init__(self):
self.openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
self.anthropic_client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
def get_response(self, message: str, model: str = "auto",
context: Optional[list] = None) -> str:
"""Get AI response using the specified model."""
if model == "auto":
# Route based on task complexity
model = self._route_message(message)
if model.startswith("gpt"):
return self._openai_response(message, model, context)
elif model.startswith("claude"):
return self._anthropic_response(message, model, context)
else:
return f"Unknown model: {model}"
def _route_message(self, message: str) -> str:
"""Route message to the best AI model based on content."""
# Coding questions → use Claude
code_keywords = ["code", "function", "debug", "error", "python",
"javascript", "typescript", "git", "api", "sql"]
if any(kw in message.lower() for kw in code_keywords):
return "claude-sonnet-4-20250514"
# General knowledge → use GPT-4o
return "gpt-4o"
def _openai_response(self, message: str, model: str, context: list) -> str:
messages = context or []
messages.append({"role": "user", "content": message})
response = self.openai_client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": (
"You are a helpful Slack AI assistant. Keep responses concise "
"because Slack messages should be scannable. Use Slack markdown "
"(*bold*, ~strike~, `code`). When sharing code, use code blocks."
)},
*messages[-10:], # Last 10 messages for context
],
max_tokens=1024,
temperature=0.7,
)
return response.choices[0].message.content
def _anthropic_response(self, message: str, model: str, context: list) -> str:
messages = context or []
messages.append({"role": "user", "content": message})
response = self.anthropic_client.messages.create(
model=model,
max_tokens=1024,
system=(
"You are a helpful Slack AI assistant. Keep responses concise. "
"Use Slack markdown formatting. Use code blocks for code snippets."
),
messages=messages[-10:],
)
return response.content[0].text
Integrate into the bot:
# app.py (updated)
from ai_handler import AIHandler
ai = AIHandler()
@app.event("app_mention")
def handle_ai_mention(event, say, context):
user = event["user"]
text = event.get("text", "")
channel = event["channel"]
bot_user_id = os.environ.get("BOT_USER_ID", "")
user_message = text.replace(f"<@{bot_user_id}>", "").strip()
if not user_message:
say("Hi! Ask me anything. Try `@ai-assistant explain Python decorators`")
return
# Show typing indicator
say(f"<@{user}> Let me look into that...")
# Get AI response
response = ai.get_response(user_message)
say(f"<@{user}> {response}")
Step 4: Add Custom Tools and Slash Commands
Build Slack slash commands for specialized tasks:
# commands.py
import os
import json
import requests
from slack_bolt import App
def register_commands(app: App, ai):
"""Register custom slash commands."""
@app.command("/summarize")
def summarize_channel(ack, respond, command, client):
"""Summarize recent channel messages using AI."""
ack()
channel_id = command["channel_id"]
user_id = command["user_id"]
# Fetch recent messages
result = client.conversations_history(
channel=channel_id,
limit=30,
)
messages = result["messages"]
# Build a summary prompt
conversation_text = "\n".join([
f"{msg.get('user', 'unknown')}: {msg.get('text', '')}"
for msg in messages
if "subtype" not in msg
])
respond("Analyzing the last 30 messages...", response_type="ephemeral")
summary = ai.get_response(
f"Summarize this Slack conversation in 3-5 bullet points:\n\n{conversation_text}"
)
respond(f"*📋 Channel Summary:*\n{summary}")
@app.command("/draft")
def draft_email(ack, respond, command, ai):
"""Draft an email or message."""
ack()
topic = command["text"]
if not topic:
respond("Usage: `/draft [topic]` — e.g., `/draft project update to stakeholders`",
response_type="ephemeral")
return
draft = ai.get_response(
f"Draft a professional email about: {topic}. Use formal tone. Include subject line."
)
respond(f"*📝 Draft:*\n{draft}")
@app.command("/explain")
def explain_code(ack, respond, command):
"""Explain code snippets."""
ack()
code = command["text"]
if not code:
respond("Usage: `/explain [your code or URL]`",
response_type="ephemeral")
return
# Check if it's a GitHub URL
if "github.com" in code:
# Fetch the raw file content
raw_url = code.replace("github.com", "raw.githubusercontent.com")
raw_url = raw_url.replace("/blob/", "/")
try:
response = requests.get(raw_url, timeout=10)
code = response.text
except:
respond("Could not fetch the file. Make sure the URL is public.",
response_type="ephemeral")
explanation = ai.get_response(f"Explain this code step by step:\n```\n{code}\n```")
respond(f"*💡 Code Explanation:*\n{explanation}")
@app.command("/tldr")
def tldr_thread(ack, respond, command, client):
"""Summarize a thread."""
ack()
channel_id = command["channel_id"]
thread_ts = command.get("thread_ts", command.get("trigger_id"))
# Try to get threaded replies
try:
result = client.conversations_replies(
channel=channel_id,
ts=thread_ts,
)
thread_text = "\n".join([
f"{msg.get('user', 'unknown')}: {msg.get('text', '')}"
for msg in result["messages"]
])
summary = ai.get_response(
f"Summarize this Slack thread in 3 points:\n\n{thread_text}"
)
respond(f"*🧵 Thread TL;DR:*\n{summary}")
except:
respond("Reply to a specific message in a thread, then use `/tldr` there.",
response_type="ephemeral")
Step 5: Add Memory and Context
Give the bot persistent memory across conversations:
# memory.py
import json
import sqlite3
from datetime import datetime, timezone
from typing import Optional, List, Dict
class BotMemory:
def __init__(self, db_path: str = "bot_memory.db"):
self.conn = sqlite3.connect(db_path)
self._init_db()
def _init_db(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id TEXT,
user_id TEXT,
message TEXT,
response TEXT,
timestamp TEXT
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS user_preferences (
user_id TEXT PRIMARY KEY,
preferred_model TEXT DEFAULT 'auto',
custom_instructions TEXT,
timezone TEXT
)
""")
self.conn.commit()
def save_conversation(self, channel_id: str, user_id: str,
message: str, response: str):
self.conn.execute(
"INSERT INTO conversations (channel_id, user_id, message, response, timestamp) "
"VALUES (?, ?, ?, ?, ?)",
(channel_id, user_id, message, response,
datetime.now(timezone.utc).isoformat()),
)
self.conn.commit()
def get_context(self, user_id: str, limit: int = 5) -> List[Dict]:
cursor = self.conn.execute(
"SELECT message, response FROM conversations "
"WHERE user_id = ? ORDER BY timestamp DESC LIMIT ?",
(user_id, limit),
)
return [
{"role": "user", "content": row[0]}
for row in cursor.fetchall()
]
def set_preference(self, user_id: str, **kwargs):
cursor = self.conn.execute(
"SELECT 1 FROM user_preferences WHERE user_id = ?", (user_id,)
)
if cursor.fetchone():
set_clause = ", ".join(f"{k} = ?" for k in kwargs)
values = list(kwargs.values()) + [user_id]
self.conn.execute(
f"UPDATE user_preferences SET {set_clause} WHERE user_id = ?",
values,
)
else:
placeholders = ", ".join(["?"] * (len(kwargs) + 1))
columns = ", ".join(["user_id"] + list(kwargs.keys()))
values = [user_id] + list(kwargs.values())
self.conn.execute(
f"INSERT INTO user_preferences ({columns}) VALUES ({placeholders})",
values,
)
self.conn.commit()
Step 6: Deploy the Bot
Deploy to a cloud server for 24/7 operation:
# docker-compose.yml
version: "3.9"
services:
slack-ai-bot:
build: .
env_file: .env
restart: always
volumes:
- ./bot_memory.db:/app/bot_memory.db
# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
Deploy with Railway or Fly.io:
# Deploy with Railway
railway up
# Or Fly.io
fly launch
fly secrets set SLACK_BOT_TOKEN=xoxb-...
fly secrets set SLACK_APP_TOKEN=xapp-...
fly deploy
Community Reviews & Ratings
Slack AI bots are a well-reviewed category:
G2: Slack rated 4.5/5 from 30,000+ reviews. AI-focused Slack apps like Ema and Ava AI each have 4.3-4.6/5 ratings. “Custom AI bots integrated via Slack Bolt are 10x more useful than generic integrations,” writes an enterprise architect.
Product Hunt: Slack bot builders like Wristband, Craft, and Botpress have 500-1,500 upvotes each. Custom AI Slack bots are a recurring top-10 category in 2025-2026.
GitHub: Slack Bolt Python SDK has 5,000+ stars. Community-built Slack bot examples on GitHub have 1-3K stars each. The most popular use cases: support triage, daily standup summaries, and code review assistants.
Capterra: The best-rated Slack AI assistants score 4.5-4.8/5. Common user feedback: “The ability to query company knowledge from within Slack has eliminated 3 hours of context-switching per day.”
Industry reports: Gartner predicts 60% of enterprises will deploy custom AI Slack bots by 2027. Forrester notes that internal AI chatbot adoption is 3x higher when integrated into existing communication tools vs standalone apps.
“Slack is the operating system of modern work. AI bots make that OS intelligent.” — Stewart Butterfield, Slack co-founder
Tips & Best Practices: Slack has tiered rate limits. Use the Bolt SDK’s built-in rate limit handling.
- Error handling: Always wrap AI calls in try/except. Show a friendly error message on failure.
- Model selection: Let users pick their model with
/set-model claude-sonnetor/set-model gpt-4o. - Cost management: Set per-user daily limits. Track token usage with logging middleware.
- Privacy: Store user preferences (not raw messages) clearly. Never log sensitive channel content.
Common Mistakes
- No typing indicator — Users won’t know the bot is working. Send an ephemeral “thinking…” message first.
- Forgetting async — Slack expects quick acknowledgments (within 3 seconds). Always ack() first, then process.
- Token leakage — Never expose API keys in bot responses. Log token usage for monitoring, not for display.
- Missing permission scopes — Each feature needs specific OAuth scopes. Check Slack API docs when adding new features.
- No context window limit — Long conversations consume tokens. Limit context to last 10-15 messages.
FAQ
Q: Can the bot see all channel messages?
Only if added to the channel and granted channels:history scope. It can’t read private channels unless invited.
Q: How much does it cost to run? Hosting on Railway: $5-10/mo. AI API costs vary: ~$10-50/mo for a small team of 10-20 users. You can set per-user budgets.
Q: Can I restrict the bot to certain channels? Yes. Add a channel allowlist in config. The bot ignores messages from channels not on the list.
Q: Does it support file analysis?
Yes — Slack’s files:read scope allows accessing shared files. Use OCR for images or read text files directly, then pass content to the AI model.
Q: Can the bot call external APIs? Yes. Add tool functions that make API calls (Jira, GitHub, Notion). The AI determines when to call them based on user intent.