What is a Slack Bot?
A Slack bot is an automated software program that interacts with users inside a Slack workspace. It can respond to slash commands, listen for specific keywords in conversations, send direct messages, post updates to channels, and even integrate with external APIs. Bots appear as special user-like entities with names, profile pictures, and a distinctive "BOT" badge, making them easily identifiable in the sidebar.
Unlike regular Slack integrations that simply push notifications, a well-built bot can carry on meaningful two-way conversations, process natural language, and perform complex workflows — from creating Jira tickets to querying databases — all without leaving the Slack interface.
Why Build a Slack Bot with Python and Flask?
Python's readability and rich ecosystem make it an ideal choice for bot development. Flask, a lightweight WSGI web framework, is perfect for exposing the HTTP endpoints that Slack requires for real-time communication. Together they offer:
- Rapid prototyping — you can have a working slash command bot in under 30 minutes
- Low infrastructure overhead — a single Flask process can handle thousands of Slack events
- Massive library support — tap into the official
slack_sdkpackage for Web API calls, Block Kit message construction, and socket mode connections - Easy debugging — Flask's built-in development server and debug mode let you inspect incoming payloads live
- Cloud-ready deployment — deploy to Heroku, Render, Fly.io, or a simple VPS with minimal configuration
Prerequisites
Before writing any code, make sure you have:
- Python 3.9 or higher installed
- A Slack workspace where you have permission to create apps (or create your own free workspace at
slack.com) ngrokinstalled for local development tunneling (or an alternative likelocaltunnel)- Basic familiarity with virtual environments and pip
Step 1: Create Your Slack App
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Head over to api.slack.com/apps and click "Create New App". Choose "From scratch", give your bot a name (e.g., "WeatherBot"), and select your target workspace. This generates the app record and brings you to the configuration dashboard.
Configure Bot Token Scopes
Navigate to "OAuth & Permissions" in the sidebar. Under "Bot Token Scopes", add the scopes your bot needs. For a basic interactive bot, start with:
chat:write— send messages to channels and userschat:write.public— send messages to public channels without joiningcommands— register and handle slash commandsapp_mentions:read— detect when someone @mentions your botreactions:read— optionally read emoji reactions
After saving, scroll up and click "Install to Workspace". Authorize the app, and you'll receive a Bot Token (starting with xoxb-). Copy this token — you'll need it shortly.
Enable Event Subscriptions
Go to "Event Subscriptions" and toggle "Enable Events". For now, leave the Request URL blank (we'll fill it after setting up ngrok). Subscribe to the app_mention event so your bot receives a payload whenever someone mentions it in a channel.
Create a Slash Command
Under "Slash Commands", click "Create New Command". Fill in:
- Command:
/weather - Request URL: leave blank for now
- Short Description: "Get current weather for a city"
- Usage Hint: "[city name]"
Save the command. Slack will later POST to your Flask endpoint whenever a user types /weather London.
Step 2: Set Up the Python Project
Create a project directory and a virtual environment:
mkdir slack-weather-bot
cd slack-weather-bot
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
Install the required packages:
pip install flask slack_sdk python-dotenv requests
Create a .env file to store secrets:
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
SLACK_SIGNING_SECRET=your-signing-secret-here
The Signing Secret is found under "Basic Information" in your Slack app dashboard. It verifies that incoming requests genuinely originate from Slack.
Step 3: Build the Flask Application
Create a file called app.py. This will be the heart of your bot. Below is the complete starter code with detailed explanations.
# app.py
import os
import json
import requests
from flask import Flask, request, jsonify, Response
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from slack_sdk.signature import SignatureVerifier
from dotenv import load_dotenv
import time
load_dotenv()
app = Flask(__name__)
# Initialize the Slack Web client for API calls
client = WebClient(token=os.environ.get("SLACK_BOT_TOKEN"))
# Signature verifier ensures requests actually come from Slack
signature_verifier = SignatureVerifier(
signing_secret=os.environ.get("SLACK_SIGNING_SECRET")
)
# --------------------- SLASH COMMAND HANDLER ---------------------
@app.route("/slack/commands", methods=["POST"])
def handle_slash_command():
"""
Slack sends a POST with form-encoded data when a user invokes
a slash command. We parse it, verify the signature, and respond
with either an immediate acknowledgment or a deferred response.
"""
# Verify that the request came from Slack
if not verify_slack_request():
return Response("Invalid signature", status=401)
# Parse form data
command = request.form.get("command", "")
text = request.form.get("text", "") # arguments after the command
user_id = request.form.get("user_id", "")
channel_id = request.form.get("channel_id", "")
response_url = request.form.get("response_url", "")
# Route based on command name
if command == "/weather":
return handle_weather_command(text, user_id, channel_id, response_url)
# Unknown command fallback
return jsonify({"text": f"Unknown command: {command}"}), 200
# --------------------- EVENT SUBSCRIPTION HANDLER ---------------------
@app.route("/slack/events", methods=["POST"])
def handle_slack_events():
"""
Slack sends event payloads as JSON. We must respond with a
200 OK within 3 seconds, then process the event asynchronously
if needed. URL verification requires echoing the challenge.
"""
# URL verification handshake (happens when you save the URL)
if request.json and request.json.get("type") == "url_verification":
challenge = request.json.get("challenge")
return jsonify({"challenge": challenge}), 200
# Verify signature for all other event types
if not verify_slack_request():
return Response("Invalid signature", status=401)
# Respond immediately to avoid timeout
event_data = request.json
# Process in a background-friendly way (here we use simple dispatch)
if event_data and event_data.get("event"):
event_type = event_data["event"]["type"]
if event_type == "app_mention":
handle_app_mention(event_data["event"])
return "", 200
# --------------------- HELPER FUNCTIONS ---------------------
def verify_slack_request():
"""
Uses Slack's signature verification to ensure authenticity.
Returns True if valid, False otherwise.
"""
body = request.get_data(as_text=True)
timestamp = request.headers.get("X-Slack-Request-Timestamp", "")
signature = request.headers.get("X-Slack-Signature", "")
# Reject requests older than 5 minutes to prevent replay attacks
if abs(time.time() - int(timestamp)) > 60 * 5:
return False
return signature_verifier.is_valid(
body=body,
timestamp=timestamp,
signature=signature
)
def handle_weather_command(city_text, user_id, channel_id, response_url):
"""
Process /weather command. Responds immediately with an ephemeral
message, then uses the response_url to post the actual weather
data asynchronously for a richer experience.
"""
if not city_text.strip():
return jsonify({
"response_type": "ephemeral",
"text": "Please provide a city name. Usage: `/weather London`"
})
# Acknowledge immediately
# We'll use response_url to send the actual weather later
# For simplicity, we do a synchronous external API call here,
# but in production you'd offload this to a background task queue.
weather_info = fetch_weather(city_text.strip())
# Post result to channel using response_url (allows delayed response)
post_via_response_url(response_url, {
"response_type": "in_channel",
"text": weather_info
})
# Return empty 200 — we already sent the message via response_url
return "", 200
def handle_app_mention(event):
"""
When someone mentions the bot, respond in the same channel.
"""
user = event.get("user", "")
channel = event.get("channel", "")
text = event.get("text", "")
# Simple keyword detection
if "weather" in text.lower():
# Extract city from mention text, e.g., "@WeatherBot weather in Paris"
parts = text.split("weather")
city = parts[-1].strip().lstrip("in ").strip() if len(parts) > 1 else ""
if city:
weather_info = fetch_weather(city)
else:
weather_info = "Which city would you like weather for? :sunny:"
elif "hello" in text.lower() or "hi" in text.lower():
weather_info = f"Hello <@{user}>! :wave: I'm WeatherBot. Try asking me about weather in any city!"
else:
weather_info = (
"I didn't quite understand that. Try:\n"
"• `@WeatherBot weather in Tokyo`\n"
"• `@WeatherBot hello`\n"
"• `/weather London`"
)
try:
client.chat_postMessage(
channel=channel,
text=weather_info,
unfurl_links=False
)
except SlackApiError as e:
print(f"Error posting message: {e.response['error']}")
def fetch_weather(city):
"""
Mock weather function — replace with a real API like OpenWeatherMap.
"""
# In production, use: requests.get(f"https://api.openweathermap.org/...")
# For demo purposes, return a static response
city_lower = city.lower()
mock_data = {
"london": "🌧️ London: 12°C, light rain, humidity 78%",
"tokyo": "☀️ Tokyo: 22°C, clear sky, humidity 45%",
"new york": "🌤️ New York: 18°C, partly cloudy, humidity 60%",
"paris": "⛅ Paris: 15°C, scattered clouds, humidity 55%",
}
return mock_data.get(
city_lower,
f"🌍 Weather for '{city}' not available in demo mode. "
f"Try: London, Tokyo, New York, or Paris."
)
def post_via_response_url(response_url, payload):
"""
Send a delayed or additional message using the response_url
provided by Slack. This URL is valid for 30 minutes.
"""
requests.post(
response_url,
headers={"Content-Type": "application/json"},
data=json.dumps(payload)
)
# --------------------- ENTRY POINT ---------------------
if __name__ == "__main__":
app.run(host="0.0.0.0", port=3000, debug=True)
Understanding the Code Flow
Here's what happens when a user types /weather London:
- Slack sends a POST to your
/slack/commandsendpoint with form data containing the command, arguments, user ID, channel ID, and a uniqueresponse_url - The
verify_slack_request()function checks theX-Slack-Signatureheader against the raw request body using HMAC-SHA256 — if mismatched, the request is rejected with a 401 - The handler routes to
handle_weather_command(), which callsfetch_weather()and posts the result using theresponse_url - The
response_urlapproach lets you go beyond the 3-second slash command timeout — you acknowledge immediately and deliver rich content later
For app_mention events, Slack sends a JSON payload to /slack/events. The handler first checks for a url_verification challenge (used when you first register the endpoint), then dispatches to handle_app_mention(), which uses the WebClient to post a threaded reply.
Step 4: Expose Your Local Server with Ngrok
Slack needs a publicly accessible HTTPS URL to reach your Flask app. During development, ngrok creates a secure tunnel to your localhost:
# In a new terminal window
ngrok http 3000
Ngrok will display a forwarding URL like https://abc123.ngrok.io. Copy this URL.
Now go back to your Slack app dashboard:
- Slash Commands: Edit the
/weathercommand and set the Request URL tohttps://abc123.ngrok.io/slack/commands - Event Subscriptions: Set the Request URL to
https://abc123.ngrok.io/slack/events— Slack will send a challenge immediately, and your Flask app'surl_verificationhandler responds correctly
Save both. If Slack shows a green verification checkmark, you're connected.
Step 5: Run and Test Your Bot
Start the Flask server:
python app.py
In your Slack workspace, test the following:
- Type
/weather Tokyoin any channel — you should see the bot's weather response appear publicly - Type
/weatherwithout arguments — you should receive an ephemeral error message visible only to you - Mention the bot with
@WeatherBot hello— it should greet you - Mention with
@WeatherBot weather in Paris— it should return Paris weather
Check your Flask terminal for any printed errors and the incoming request logs. Debug mode auto-reloads on code changes, so iteration is fast.
Step 6: Add Rich Formatting with Block Kit
Plain text responses work, but Block Kit lets you build richly structured messages with sections, dividers, buttons, and images. Here's how to upgrade the weather response:
def build_weather_blocks(city, temperature, condition, humidity):
"""
Construct a Block Kit message for a weather report.
"""
blocks = [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"🌤️ Weather for {city}",
"emoji": True
}
},
{
"type": "divider"
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Temperature:*\n{temperature}"
},
{
"type": "mrkdwn",
"text": f"*Condition:*\n{condition}"
},
{
"type": "mrkdwn",
"text": f"*Humidity:*\n{humidity}%"
}
]
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Refresh",
"emoji": True
},
"value": f"refresh_{city}",
"action_id": "weather_refresh"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "Share",
"emoji": True
},
"value": f"share_{city}",
"action_id": "weather_share"
}
]
}
]
return blocks
# Updated fetch_weather using blocks
def fetch_weather_with_blocks(city):
"""
Fetch weather and return Block Kit payload.
"""
# ... API call logic here ...
blocks = build_weather_blocks(city, "18°C", "Partly Cloudy", "65")
return blocks
# Updated slash command handler
def handle_weather_command(city_text, user_id, channel_id, response_url):
if not city_text.strip():
return jsonify({
"response_type": "ephemeral",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "⚠️ Please provide a city name. Usage: `/weather London`"
}
}
]
})
blocks = fetch_weather_with_blocks(city_text.strip())
post_via_response_url(response_url, {
"response_type": "in_channel",
"blocks": blocks,
"text": f"Weather update for {city_text}" # fallback for notifications
})
return "", 200
Block Kit messages require a text fallback field for notification previews and accessibility. The blocks array defines the visual layout. Interactive elements like buttons require an additional endpoint to handle their action_id callbacks — Slack sends an interactive_message payload to your configured Interactions URL.
Step 7: Handle Interactive Messages (Buttons)
To respond to button clicks, add a new endpoint and register it in your Slack app dashboard under "Interactivity & Shortcuts":
@app.route("/slack/interactions", methods=["POST"])
def handle_interactions():
"""
Handle button clicks, dropdown selections, and modal submissions.
Slack sends the payload as form-encoded JSON in the 'payload' field.
"""
# Verify signature
if not verify_slack_request():
return Response("Invalid signature", status=401)
payload = json.loads(request.form.get("payload", "{}"))
# Check if it's a block action
if payload.get("type") == "block_actions":
actions = payload.get("actions", [])
for action in actions:
action_id = action.get("action_id")
if action_id == "weather_refresh":
city = action.get("value", "").replace("refresh_", "")
blocks = fetch_weather_with_blocks(city)
# Update the original message (or post new)
response_url = payload.get("response_url", "")
post_via_response_url(response_url, {
"replace_original": "true",
"blocks": blocks,
"text": f"Refreshed weather for {city}"
})
elif action_id == "weather_share":
city = action.get("value", "").replace("share_", "")
# Post a new message to the channel
client.chat_postMessage(
channel=payload["channel"]["id"],
text=f"Someone shared the weather for {city}!",
blocks=fetch_weather_with_blocks(city)
)
return "", 200
Set the Interactions Request URL in your Slack app to https://abc123.ngrok.io/slack/interactions. Now button clicks trigger your handler, enabling dynamic message updates without a full page reload on the Slack client side.
Best Practices for Slack Bot Development
1. Always Verify Signatures
Never skip the verify_slack_request() step. Without it, anyone could spoof Slack payloads to your endpoint, potentially posting unwanted messages to your workspace. The signing secret is the cryptographic guarantee that the request originated from Slack's servers.
2. Respond Within 3 Seconds (or Defer)
Slash commands and event subscriptions have a strict 3-second timeout. If your processing takes longer, Slack returns an error to the user. Use the response_url pattern shown above to acknowledge immediately and deliver results asynchronously. For even heavier workloads, queue the task with Celery or RQ and have the worker call the response_url upon completion.
3. Use Ephemeral Messages for Errors
When a user makes a mistake (like forgetting a required argument), respond with "response_type": "ephemeral". This shows the error only to that user, keeping channels clean. Reserve "in_channel" for results that benefit the whole team.
4. Handle Rate Limiting Gracefully
Slack's Web API enforces tiered rate limits. The slack_sdk WebClient includes built-in retry logic with exponential backoff, but you should still implement try/except blocks and log SlackApiError exceptions. For high-volume bots, track your request counts and implement queues to smooth out bursts.
5. Store Tokens Securely
Never hardcode tokens. Use environment variables (python-dotenv for local dev, platform secrets manager for production). Rotate tokens periodically. The Bot Token grants broad access — treat it like a password.
6. Implement Proper Logging
Add structured logging with timestamps, request IDs, and error details. This is invaluable when debugging why a slash command failed silently:
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)
# Inside handlers:
logger.info(f"Processing /weather command from user={user_id} with text='{text}'")
7. Test Thoroughly with Multiple Workspaces
Install your app to a secondary test workspace in addition to your primary one. This lets you verify that scopes, event subscriptions, and command registrations work independently of your personal settings and channel memberships.
8. Handle Reconnects and Socket Mode
For bots that need to listen to many real-time events without exposing a public HTTP endpoint, Slack offers Socket Mode. Instead of HTTP POSTs, your bot opens a WebSocket connection to Slack and receives events over it. The slack_sdk supports this via SocketModeHandler:
from slack_sdk.socket_mode import SocketModeHandler
from slack_sdk.socket_mode.request import SocketModeRequest
def handle_socket_event(client: WebClient, req: SocketModeRequest):
if req.type == "slash_commands":
# Process slash command via socket
pass
elif req.type == "events_api":
# Process event
pass
handler = SocketModeHandler(app_token="xapp-...", web_client=client)
handler.connect()
handler.start() # Blocks until interrupted
Socket Mode eliminates the need for ngrok during development and simplifies firewall configurations, though it requires an App-level token with connections:write scope.
9. Version Your API Endpoints
As your bot evolves, you may change the payload structure of your responses. Prefix your endpoints with version numbers (e.g., /v1/slack/commands) or include a version field in your responses so older clients don't break unexpectedly.
10. Document Your Bot Commands
Create a help command that lists all available interactions:
if command == "/weather-help":
help_text = (
"*Available commands:*\n"
"• `/weather [city]` — Get current weather\n"
"• `/weather-help` — Show this help message\n"
"• `@WeatherBot weather in [city]` — Mention-based weather query\n"
"• `@WeatherBot hello` — Greeting"
)
return jsonify({"text": help_text, "response_type": "ephemeral"})
Deployment Considerations
When you're ready to move beyond ngrok, deploy your Flask app to a production server. Popular options include:
- Heroku — add a
Procfilewithweb: gunicorn app:app, set environment variables via the dashboard, and use the free SSL endpoint - Render — native Python support, auto-deploys from Git, built-in TLS
- Fly.io — global edge deployment with automatic SSL certificates
- AWS Lambda + API Gateway — serverless approach using
slack_boltadapter for Lambda
Update your Slack app's Request URLs to point to the production domain, reinstall the app if scope changes occurred, and you're live.
Conclusion
Building a Slack bot with Python and Flask unlocks powerful automation directly within your team's communication hub. You've learned how to create a Slack app from scratch, configure OAuth scopes, handle slash commands and event subscriptions, verify request signatures, construct rich Block Kit messages, process interactive button clicks, and follow production-grade best practices. The combination of Flask's simplicity and the Slack SDK's comprehensive API coverage lets you build anything from a weather reporter to a full-fledged incident management bot. Start with the patterns shown here, iterate based on your team's feedback, and you'll have a bot that genuinely improves daily workflows.