Python Slack Bot Development — Deep Dive
System-level framing
A production Slack bot is a microservice that integrates with Slack’s event delivery system. It needs to handle concurrent events, respond within Slack’s 3-second timeout, manage OAuth for multi-workspace installs, and degrade gracefully when downstream services are unavailable. The Bolt framework handles the protocol layer; your job is the reliability layer.
Setting up with Bolt
Socket Mode (quick start)
import os
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
app = App(token=os.environ["SLACK_BOT_TOKEN"])
@app.message("hello")
def handle_hello(message, say):
user = message["user"]
say(f"Hey <@{user}>! How can I help?")
@app.command("/deploy")
def handle_deploy(ack, command, say):
ack() # Acknowledge within 3 seconds
env = command["text"].strip() or "staging"
user = command["user_id"]
say(f"<@{user}> triggered deploy to *{env}*. Starting...")
# Run actual deployment logic
result = deploy_to_environment(env)
say(f"Deploy to *{env}*: {'succeeded' if result else 'failed'}")
if __name__ == "__main__":
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()
Socket Mode requires an App-Level Token (xapp-…) in addition to the Bot Token. Generate it in your app settings under “Basic Information.”
HTTP mode for production
from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler
from flask import Flask, request
bolt_app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"]
)
flask_app = Flask(__name__)
handler = SlackRequestHandler(bolt_app)
@flask_app.route("/slack/events", methods=["POST"])
def slack_events():
return handler.handle(request)
@flask_app.route("/slack/commands", methods=["POST"])
def slack_commands():
return handler.handle(request)
@flask_app.route("/slack/interactions", methods=["POST"])
def slack_interactions():
return handler.handle(request)
The signing_secret validates that incoming requests genuinely come from Slack, preventing replay attacks.
Block Kit: rich messaging
Structured messages
@app.command("/incident")
def handle_incident(ack, command, client):
ack()
client.chat_postMessage(
channel=command["channel_id"],
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": "🚨 Incident Report"}
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f"*Reporter:*\n<@{command['user_id']}>"},
{"type": "mrkdwn", "text": f"*Severity:*\nPending"},
{"type": "mrkdwn", "text": f"*Status:*\nInvestigating"},
{"type": "mrkdwn", "text": f"*Time:*\n<!date^{int(time.time())}^{'{date_short} {time}'}|now>"}
]
},
{
"type": "actions",
"elements": [
{
"type": "static_select",
"placeholder": {"type": "plain_text", "text": "Set severity"},
"action_id": "set_severity",
"options": [
{"text": {"type": "plain_text", "text": "SEV1 - Critical"}, "value": "sev1"},
{"text": {"type": "plain_text", "text": "SEV2 - Major"}, "value": "sev2"},
{"text": {"type": "plain_text", "text": "SEV3 - Minor"}, "value": "sev3"},
]
},
{
"type": "button",
"text": {"type": "plain_text", "text": "Acknowledge"},
"style": "primary",
"action_id": "ack_incident"
}
]
}
],
text="New incident reported" # Fallback for notifications
)
Handling interactions
@app.action("set_severity")
def handle_severity(ack, action, body, client):
ack()
severity = action["selected_option"]["value"]
# Update the original message
client.chat_update(
channel=body["channel"]["id"],
ts=body["message"]["ts"],
blocks=update_severity_in_blocks(body["message"]["blocks"], severity),
text=f"Incident severity set to {severity}"
)
@app.action("ack_incident")
def handle_ack(ack, action, body, client):
ack()
user = body["user"]["id"]
client.chat_update(
channel=body["channel"]["id"],
ts=body["message"]["ts"],
blocks=mark_acknowledged(body["message"]["blocks"], user),
text=f"Incident acknowledged by <@{user}>"
)
Modals for complex input
@app.command("/feedback")
def open_feedback_modal(ack, command, client):
ack()
client.views_open(
trigger_id=command["trigger_id"],
view={
"type": "modal",
"callback_id": "feedback_submission",
"title": {"type": "plain_text", "text": "Submit Feedback"},
"submit": {"type": "plain_text", "text": "Submit"},
"blocks": [
{
"type": "input",
"block_id": "category_block",
"element": {
"type": "static_select",
"action_id": "category",
"options": [
{"text": {"type": "plain_text", "text": "Bug"}, "value": "bug"},
{"text": {"type": "plain_text", "text": "Feature"}, "value": "feature"},
{"text": {"type": "plain_text", "text": "Improvement"}, "value": "improvement"},
]
},
"label": {"type": "plain_text", "text": "Category"}
},
{
"type": "input",
"block_id": "details_block",
"element": {
"type": "plain_text_input",
"action_id": "details",
"multiline": True
},
"label": {"type": "plain_text", "text": "Details"}
}
]
}
)
@app.view("feedback_submission")
def handle_feedback(ack, view, client):
ack()
values = view["state"]["values"]
category = values["category_block"]["category"]["selected_option"]["value"]
details = values["details_block"]["details"]["value"]
user = view["user"]["id"]
# Save to database, create issue, etc.
save_feedback(user, category, details)
# Notify the user
client.chat_postMessage(
channel=user,
text=f"Thanks for your {category} feedback! We'll review it soon."
)
Middleware for cross-cutting concerns
from slack_bolt import BoltContext
import logging
logger = logging.getLogger(__name__)
@app.middleware
def log_request(body, next, logger):
"""Log every incoming event for debugging."""
event_type = body.get("type", "unknown")
user = body.get("event", {}).get("user", "system")
logger.info(f"Event: {event_type}, User: {user}")
next()
@app.middleware
def check_allowed_channels(body, next, context: BoltContext):
"""Restrict bot to specific channels."""
allowed = os.environ.get("ALLOWED_CHANNELS", "").split(",")
channel = body.get("event", {}).get("channel", "")
if allowed and allowed[0] and channel not in allowed:
return # Silently ignore events from non-allowed channels
next()
Handling the 3-second timeout
Slack requires acknowledgment within 3 seconds for slash commands and interactions. For long-running tasks, acknowledge immediately and process asynchronously:
import threading
@app.command("/report")
def handle_report(ack, command, say, client):
ack("Generating your report... this might take a minute.")
def generate_async():
# This runs in a background thread
report = generate_heavy_report(command["text"])
client.chat_postMessage(
channel=command["channel_id"],
text=f"<@{command['user_id']}> here's your report:\n{report}"
)
threading.Thread(target=generate_async).start()
For production, replace threading with a proper task queue (Celery, RQ) to handle retries and monitoring.
Multi-workspace OAuth
For bots distributed to multiple Slack workspaces:
from slack_bolt import App
from slack_bolt.oauth.oauth_settings import OAuthSettings
from slack_sdk.oauth.installation_store import FileInstallationStore
from slack_sdk.oauth.state_store import FileOAuthStateStore
app = App(
signing_secret=os.environ["SLACK_SIGNING_SECRET"],
oauth_settings=OAuthSettings(
client_id=os.environ["SLACK_CLIENT_ID"],
client_secret=os.environ["SLACK_CLIENT_SECRET"],
scopes=["chat:write", "commands", "app_mentions:read"],
installation_store=FileInstallationStore(base_dir="./data/installations"),
state_store=FileOAuthStateStore(expiration_seconds=600, base_dir="./data/states"),
)
)
For production, replace FileInstallationStore with a database-backed store (SQLAlchemy, DynamoDB, etc.).
Testing
import pytest
from slack_bolt import App
from slack_bolt.tests.mock_web_api_server import setup_mock_web_api_server
def test_hello_handler():
app = App(token="xoxb-test", signing_secret="test-secret")
@app.message("hello")
def handle_hello(message, say):
say(f"Hey <@{message['user']}>!")
# Simulate an event
from unittest.mock import MagicMock
say_mock = MagicMock()
handle_hello({"user": "U123"}, say_mock)
say_mock.assert_called_once_with("Hey <@U123>!")
Separate your business logic from Slack handlers so the core logic is testable with plain unit tests, and only the thin handler layer needs Slack mocking.
The one thing to remember: Production Slack bots need immediate acknowledgment (3-second rule), async processing for heavy tasks, Block Kit for rich interactions, and proper OAuth for multi-workspace distribution — the Bolt framework gives you the plumbing, but reliability is your responsibility.
See Also
- Python Discord Bot Development Learn how Python creates Discord bots that moderate servers, play music, and respond to commands — explained for total beginners.
- Python Email Templating Jinja Discover how Jinja templates let Python create personalized emails for thousands of people without writing each one by hand.
- Python Imap Reading Emails See how Python reads your inbox using IMAP — explained with a mailbox-and-key analogy anyone can follow.
- Python Push Notifications How Python sends those buzzing alerts to your phone and browser — explained for anyone who has ever wondered where notifications come from.
- Python Smtplib Sending Emails Understand how Python sends emails through smtplib using the simplest real-world analogy you will ever need.