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.

pythonslackbotsautomation

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.