LabHub

Blog

Slack Bot Building Practical Guide — Python Bolt SDK, Event Handling, Slash Commands

한국어English日本語

Introduction

Slack is a workplace communication platform used by hundreds of thousands of teams around the world. Beyond simple messaging, a Slack Bot lets you automate all sorts of work: incident alerts, deployment automation, data lookups, approval workflows and more.

Bolt for Python, Slack's official framework, lets you implement every Slack interaction - event handling, slash commands, modals, Block Kit - with a concise decorator pattern. This article covers everything you need in practice, from app setup to production deployment, error handling and failure recovery.


1. Slack App Architecture and Setup

1.1 Slack App Architecture Overview

The flow of a Slack Bot works as follows.

  1. The user performs an interaction in Slack - a message, a slash command, a button click
  2. The Slack server forwards the event to the bot server (over HTTP or WebSocket)
  3. The bot handles the event and responds through the Slack Web API
  4. The result is shown to the user as a message, a modal or a rich block

1.2 Creating the App and Configuring OAuth

# Step 1: create the app on the Slack API site
# go to https://api.slack.com/apps
# "Create New App" -> choose "From scratch"
# App Name: "MySlackBot", choose the workspace

# Step 2: set the Bot Token Scopes under OAuth & Permissions
# the scopes you need:
#   chat:write          - send messages
#   chat:write.public   - send messages to public channels the bot has not joined
#   commands            - register slash commands
#   im:history          - read DM messages
#   im:read             - access DM channel information
#   channels:history    - read public channel messages
#   channels:read       - list channels and read channel info
#   users:read          - read user profiles
#   reactions:read      - read reaction information
#   reactions:write     - add reactions
#   files:write         - upload files

# Step 3: enable Event Subscriptions
# Subscribe to bot events:
#   message.im          - receive DMs
#   message.channels    - receive channel messages
#   app_mention         - receive @bot mentions
#   app_home_opened     - the app home tab is opened
#   reaction_added      - reaction added event
#   reaction_removed    - reaction removed event

# Step 4: enable Interactivity & Shortcuts
# required to use interactive features such as modals, buttons and select menus

# Step 5: enable Socket Mode (for development / simple deployment)
# Settings -> Socket Mode -> Enable Socket Mode
# create an App-Level Token (connections:write scope)

A caution: the Bot Token (xoxb-) and the App Token (xapp-) serve different purposes. The Bot Token is used to call the Slack Web API; the App Token is used for the Socket Mode WebSocket connection.

1.3 The Token Types

Token typePrefixPurposeWhere to get it
Bot Tokenxoxb-Slack API calls (messages, channels and so on)OAuth & Permissions
App Tokenxapp-Socket Mode WebSocket connectionBasic Information
User Tokenxoxp-API calls made with the user's permissionsOAuth & Permissions
Signing Secret-Verifying the signature of HTTP requestsBasic Information

2. Installing the Python Bolt SDK and the Basic Setup

2.1 Project Initialization

# Python 3.9+ recommended
python -m venv .venv
source .venv/bin/activate

# install the core dependencies
pip install slack-bolt==1.27.0 slack-sdk python-dotenv

# project directory structure
mkdir -p slack-bot/{handlers,services,utils}
touch slack-bot/{app.py,.env,requirements.txt}
touch slack-bot/handlers/{__init__.py,commands.py,events.py,actions.py,modals.py}
touch slack-bot/services/{__init__.py,notification.py}
touch slack-bot/utils/{__init__.py,rate_limiter.py,logger.py}

# check the directory structure
# slack-bot/
# ├── app.py                 # main app entry point
# ├── .env                   # environment variables (tokens and so on)
# ├── requirements.txt
# ├── Dockerfile
# ├── handlers/
# │   ├── __init__.py
# │   ├── commands.py        # slash command handlers
# │   ├── events.py          # event handlers (messages, mentions)
# │   ├── actions.py         # button/select action handlers
# │   └── modals.py          # modal view handlers
# ├── services/
# │   ├── __init__.py
# │   └── notification.py    # notification service
# └── utils/
#     ├── __init__.py
#     ├── rate_limiter.py    # rate limit utilities
#     └── logger.py          # logging configuration

2.2 The Basic Main App Setup

# app.py
import os
import logging
from dotenv import load_dotenv
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

load_dotenv()

# logging configuration
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)

# initialize the Bolt App
app = App(
    token=os.environ["SLACK_BOT_TOKEN"],
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
)


# ===== global middleware =====
@app.middleware
def log_request(logger, body, next):
    """Middleware that logs every request"""
    logger.info(f"Request type: {body.get('type', 'unknown')}")
    next()


# ===== register the event handlers =====
from handlers.events import register_event_handlers
from handlers.commands import register_command_handlers
from handlers.actions import register_action_handlers
from handlers.modals import register_modal_handlers

register_event_handlers(app)
register_command_handlers(app)
register_action_handlers(app)
register_modal_handlers(app)


# ===== global error handler =====
@app.error
def global_error_handler(error, body, logger):
    logger.exception(f"Unhandled error: {error}")
    logger.debug(f"Request body: {body}")


# ===== run =====
if __name__ == "__main__":
    logger.info("Slack Bot starting in Socket Mode...")
    handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
    handler.start()

3. Event Handling -- Messages, Mentions, Reactions

Event handling is the heart of a Slack Bot. The Bolt SDK registers event listeners through a decorator pattern.

3.1 Implementing the Event Handlers

# handlers/events.py
import re
from datetime import datetime


def register_event_handlers(app):
    """Register the event handlers on the app"""

    # --- handle app mentions ---
    @app.event("app_mention")
    def handle_app_mention(event, say, client, logger):
        """Called when the bot is @-mentioned"""
        user_id = event["user"]
        text = event.get("text", "")
        channel = event["channel"]

        # strip the bot ID out of the mention text
        clean_text = re.sub(r"<@\w+>", "", text).strip()

        try:
            user_info = client.users_info(user=user_id)
            display_name = user_info["user"]["real_name"]
        except Exception as e:
            logger.error(f"Failed to fetch user info: {e}")
            display_name = "user"

        if "help" in clean_text.lower():
            say(
                channel=channel,
                blocks=[
                    {
                        "type": "section",
                        "text": {
                            "type": "mrkdwn",
                            "text": (
                                f"Hello {display_name}!\n\n"
                                "*Available commands:*\n"
                                "- `/task` - create a task\n"
                                "- `/status` - check service status\n"
                                "- `/oncall` - check who is on call\n"
                                "- Feel free to ask anything by DM!"
                            ),
                        },
                    }
                ],
            )
        else:
            say(
                channel=channel,
                text=f"{display_name}, I will look into what you said.",
                thread_ts=event.get("ts"),  # reply in a thread
            )

    # --- handle DM messages ---
    @app.event("message")
    def handle_message(event, say, logger):
        """Handle channel/DM message events"""
        # ignore the bot's own messages and edit/delete events
        if event.get("bot_id") or event.get("subtype"):
            return

        channel_type = event.get("channel_type", "")
        text = event.get("text", "")
        user_id = event.get("user", "")

        # auto-reply to DMs only
        if channel_type == "im":
            logger.info(f"DM from {user_id}: {text[:50]}...")

            # branch on keywords
            if "incident" in text.lower() or "outage" in text.lower():
                say(
                    text="Would you like to file an incident report?",
                    blocks=[
                        {
                            "type": "section",
                            "text": {
                                "type": "mrkdwn",
                                "text": "Would you like to file an incident report?",
                            },
                            "accessory": {
                                "type": "button",
                                "text": {"type": "plain_text", "text": "Report incident"},
                                "action_id": "open_incident_modal",
                                "style": "danger",
                            },
                        }
                    ],
                )
            else:
                say(f"Got your message. If you need more help, type `/help`.")

    # --- handle reaction events ---
    @app.event("reaction_added")
    def handle_reaction_added(event, client, logger):
        """Handle the reaction_added event (e.g. trigger work from an emoji)"""
        reaction = event["reaction"]
        item = event["item"]
        user_id = event["user"]

        logger.info(f"Reaction :{reaction}: added by {user_id}")

        # mark the task complete on a :white_check_mark: reaction
        if reaction == "white_check_mark":
            client.chat_postMessage(
                channel=item["channel"],
                thread_ts=item["ts"],
                text=f"<@{user_id}> marked this task as complete.",
            )

        # mark the task as being looked at on an :eyes: reaction
        elif reaction == "eyes":
            client.chat_postMessage(
                channel=item["channel"],
                thread_ts=item["ts"],
                text=f"<@{user_id}> is looking into this.",
            )

    # --- app home tab ---
    @app.event("app_home_opened")
    def handle_app_home_opened(client, event, logger):
        """Show the dashboard when the app home tab is opened"""
        user_id = event["user"]
        now = datetime.now().strftime("%Y-%m-%d %H:%M")

        try:
            client.views_publish(
                user_id=user_id,
                view={
                    "type": "home",
                    "blocks": [
                        {
                            "type": "header",
                            "text": {
                                "type": "plain_text",
                                "text": "Bot Dashboard",
                            },
                        },
                        {
                            "type": "section",
                            "text": {
                                "type": "mrkdwn",
                                "text": (
                                    "*Available features:*\n"
                                    "- `/task` : create and manage tasks\n"
                                    "- `/status` : check service status\n"
                                    "- `/oncall` : look up who is on call\n"
                                    "- DM : free-form Q&A"
                                ),
                            },
                        },
                        {"type": "divider"},
                        {
                            "type": "context",
                            "elements": [
                                {
                                    "type": "mrkdwn",
                                    "text": f"Last updated: {now}",
                                }
                            ],
                        },
                    ],
                },
            )
        except Exception as e:
            logger.error(f"Failed to publish home tab: {e}")

Key points:


4. Implementing Slash Commands

A slash command is how a user invokes a bot feature by typing /command.

4.1 A Basic Command Wired to a Modal

# handlers/commands.py
import json
from datetime import datetime


def register_command_handlers(app):

    @app.command("/task")
    def handle_task_command(ack, body, client, logger):
        """A slash command that opens the task creation modal"""
        # ack() must be called within 3 seconds!
        ack()

        try:
            client.views_open(
                trigger_id=body["trigger_id"],
                view={
                    "type": "modal",
                    "callback_id": "task_create_modal",
                    "title": {"type": "plain_text", "text": "Create task"},
                    "submit": {"type": "plain_text", "text": "Create"},
                    "close": {"type": "plain_text", "text": "Cancel"},
                    "blocks": [
                        {
                            "type": "input",
                            "block_id": "title_block",
                            "element": {
                                "type": "plain_text_input",
                                "action_id": "title_input",
                                "placeholder": {
                                    "type": "plain_text",
                                    "text": "Enter the task title",
                                },
                            },
                            "label": {"type": "plain_text", "text": "Title"},
                        },
                        {
                            "type": "input",
                            "block_id": "priority_block",
                            "element": {
                                "type": "static_select",
                                "action_id": "priority_select",
                                "options": [
                                    {
                                        "text": {"type": "plain_text", "text": "P1 - Urgent"},
                                        "value": "P1",
                                    },
                                    {
                                        "text": {"type": "plain_text", "text": "P2 - High"},
                                        "value": "P2",
                                    },
                                    {
                                        "text": {"type": "plain_text", "text": "P3 - Normal"},
                                        "value": "P3",
                                    },
                                    {
                                        "text": {"type": "plain_text", "text": "P4 - Low"},
                                        "value": "P4",
                                    },
                                ],
                            },
                            "label": {"type": "plain_text", "text": "Priority"},
                        },
                        {
                            "type": "input",
                            "block_id": "assignee_block",
                            "element": {
                                "type": "users_select",
                                "action_id": "assignee_select",
                                "placeholder": {
                                    "type": "plain_text",
                                    "text": "Choose an assignee",
                                },
                            },
                            "label": {"type": "plain_text", "text": "Assignee"},
                        },
                        {
                            "type": "input",
                            "block_id": "desc_block",
                            "element": {
                                "type": "plain_text_input",
                                "action_id": "desc_input",
                                "multiline": True,
                                "placeholder": {
                                    "type": "plain_text",
                                    "text": "Describe the task in detail",
                                },
                            },
                            "label": {"type": "plain_text", "text": "Description"},
                            "optional": True,
                        },
                    ],
                },
            )
        except Exception as e:
            logger.error(f"Failed to open modal: {e}")

    @app.command("/status")
    def handle_status_command(ack, say, command, logger):
        """A command that reports the service status"""
        ack()

        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        say(
            blocks=[
                {
                    "type": "header",
                    "text": {"type": "plain_text", "text": "Service Status Dashboard"},
                },
                {
                    "type": "section",
                    "fields": [
                        {"type": "mrkdwn", "text": "*API Server*\nHealthy (v2.4.1)"},
                        {"type": "mrkdwn", "text": "*Web Frontend*\nHealthy (v3.1.0)"},
                        {"type": "mrkdwn", "text": "*Worker*\nDegraded (v1.8.2)"},
                        {"type": "mrkdwn", "text": "*Database*\nHealthy (CPU 38%)"},
                    ],
                },
                {"type": "divider"},
                {
                    "type": "actions",
                    "elements": [
                        {
                            "type": "button",
                            "text": {"type": "plain_text", "text": "View details"},
                            "action_id": "status_detail",
                            "value": "all",
                        },
                        {
                            "type": "button",
                            "text": {"type": "plain_text", "text": "Refresh"},
                            "action_id": "status_refresh",
                        },
                    ],
                },
                {
                    "type": "context",
                    "elements": [
                        {
                            "type": "mrkdwn",
                            "text": f"Queried at: {now}",
                        }
                    ],
                },
            ],
        )

    @app.command("/oncall")
    def handle_oncall_command(ack, say, command, logger):
        """A command that reports who is currently on call"""
        ack()

        # in reality this would integrate with an external service such as PagerDuty or OpsGenie
        say(
            blocks=[
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": (
                            "*Currently on call*\n\n"
                            "- Backend: <@U12345678>\n"
                            "- Frontend: <@U23456789>\n"
                            "- Infra: <@U34567890>\n\n"
                            "For anything urgent, DM the person on call."
                        ),
                    },
                }
            ],
        )

ack() must be called: Slack expects an acknowledge response within 3 seconds of sending a slash command. If you do not call ack(), the user is shown an error saying the slash command could not be processed.


5. Modals and Interactive Components

5.1 Handling a Modal Submission

# handlers/modals.py
from datetime import datetime


def register_modal_handlers(app):

    @app.view("task_create_modal")
    def handle_task_modal_submission(ack, body, client, view, logger):
        """Handle the task creation modal submission"""
        # validate the input
        values = view["state"]["values"]
        title = values["title_block"]["title_input"]["value"]
        errors = {}

        if len(title) < 3:
            errors["title_block"] = "The title must be at least 3 characters."

        if errors:
            ack(response_action="errors", errors=errors)
            return

        ack()

        # extract the values
        priority = values["priority_block"]["priority_select"]["selected_option"]["value"]
        assignee = values["assignee_block"]["assignee_select"]["selected_user"]
        description = values["desc_block"]["desc_input"].get("value", "No description")
        creator = body["user"]["id"]

        # send the task-created message to the notification channel
        now = datetime.now().strftime("%Y-%m-%d %H:%M")

        client.chat_postMessage(
            channel="#tasks",
            blocks=[
                {
                    "type": "header",
                    "text": {"type": "plain_text", "text": "A new task has been created"},
                },
                {
                    "type": "section",
                    "fields": [
                        {"type": "mrkdwn", "text": f"*Title:*\n{title}"},
                        {"type": "mrkdwn", "text": f"*Priority:*\n{priority}"},
                        {"type": "mrkdwn", "text": f"*Assignee:*\n<@{assignee}>"},
                        {"type": "mrkdwn", "text": f"*Created by:*\n<@{creator}>"},
                    ],
                },
                {
                    "type": "section",
                    "text": {"type": "mrkdwn", "text": f"*Description:*\n{description}"},
                },
                {"type": "divider"},
                {
                    "type": "actions",
                    "elements": [
                        {
                            "type": "button",
                            "text": {"type": "plain_text", "text": "Start task"},
                            "style": "primary",
                            "action_id": "task_start",
                            "value": title,
                        },
                        {
                            "type": "button",
                            "text": {"type": "plain_text", "text": "Complete"},
                            "action_id": "task_complete",
                            "value": title,
                        },
                    ],
                },
                {
                    "type": "context",
                    "elements": [
                        {"type": "mrkdwn", "text": f"Created at: {now}"}
                    ],
                },
            ],
        )

        # DM the assignee
        client.chat_postMessage(
            channel=assignee,
            text=f"A new task has been assigned to you: *{title}* (priority: {priority})\nCreated by: <@{creator}>",
        )

5.2 Handling Button Actions

# handlers/actions.py


def register_action_handlers(app):

    @app.action("task_start")
    def handle_task_start(ack, body, client, action, logger):
        """Handle a click on the Start task button"""
        ack()
        user_id = body["user"]["id"]
        task_title = action["value"]

        # update the original message
        client.chat_update(
            channel=body["channel"]["id"],
            ts=body["message"]["ts"],
            blocks=[
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": (
                            f"*{task_title}*\n"
                            f"Status: in progress\n"
                            f"Owner: <@{user_id}>"
                        ),
                    },
                },
                {
                    "type": "actions",
                    "elements": [
                        {
                            "type": "button",
                            "text": {"type": "plain_text", "text": "Mark complete"},
                            "style": "primary",
                            "action_id": "task_complete",
                            "value": task_title,
                        },
                    ],
                },
            ],
        )

    @app.action("task_complete")
    def handle_task_complete(ack, body, client, action, logger):
        """Handle a click on the Complete button"""
        ack()
        user_id = body["user"]["id"]
        task_title = action["value"]

        client.chat_update(
            channel=body["channel"]["id"],
            ts=body["message"]["ts"],
            blocks=[
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": (
                            f"~{task_title}~\n"
                            f"Status: complete\n"
                            f"Completed by: <@{user_id}>"
                        ),
                    },
                },
            ],
        )

    @app.action("open_incident_modal")
    def handle_open_incident(ack, body, client, logger):
        """Open the incident report modal"""
        ack()
        client.views_open(
            trigger_id=body["trigger_id"],
            view={
                "type": "modal",
                "callback_id": "incident_report_modal",
                "title": {"type": "plain_text", "text": "Report incident"},
                "submit": {"type": "plain_text", "text": "Report"},
                "blocks": [
                    {
                        "type": "input",
                        "block_id": "severity_block",
                        "element": {
                            "type": "static_select",
                            "action_id": "severity_select",
                            "options": [
                                {
                                    "text": {"type": "plain_text", "text": "P1 - Critical"},
                                    "value": "P1",
                                },
                                {
                                    "text": {"type": "plain_text", "text": "P2 - Major"},
                                    "value": "P2",
                                },
                                {
                                    "text": {"type": "plain_text", "text": "P3 - Minor"},
                                    "value": "P3",
                                },
                            ],
                        },
                        "label": {"type": "plain_text", "text": "Severity"},
                    },
                    {
                        "type": "input",
                        "block_id": "incident_desc_block",
                        "element": {
                            "type": "plain_text_input",
                            "action_id": "incident_desc_input",
                            "multiline": True,
                            "placeholder": {
                                "type": "plain_text",
                                "text": "Describe the incident in detail",
                            },
                        },
                        "label": {"type": "plain_text", "text": "Description of the situation"},
                    },
                ],
            },
        )

    @app.action("status_refresh")
    def handle_status_refresh(ack, body, logger):
        """The status refresh button"""
        ack()
        logger.info("Status refresh requested")

    @app.action("status_detail")
    def handle_status_detail(ack, body, logger):
        """The status details button"""
        ack()
        logger.info("Status detail requested")

6. Rich Messages with Block Kit

Block Kit is Slack's UI framework for composing structured, interactive messages. The main block types are as follows.

Block typePurposeWhere it is used
sectionText, fields, an accessory (a button and so on)Messages, modals
actionsButtons, select menus, date pickersMessages
inputUser input (text, select)Modals only
headerA large text headingMessages, modals
dividerA divider lineMessages, modals
contextSmall text, imagesMessages, modals
imageDisplay an imageMessages, modals
rich_textFormatted textMessages

The Block Kit Builder (https://app.slack.com/block-kit-builder) lets you compose blocks visually and preview the JSON.


7. Socket Mode vs HTTP Mode

CategorySocket ModeHTTP Mode
ConnectionWebSocket (bidirectional)HTTP POST (one-way)
Public URL neededNot neededNeeded (an HTTPS endpoint)
FirewallWorks from behind a firewallMust be reachable from outside
TokensApp Token (xapp-) + Bot TokenBot Token + Signing Secret
Local developmentVery convenient (no ngrok)Needs a tunneling tool such as ngrok
ScalabilityA single instance is recommendedMultiple instances can be load-balanced
StabilityNeeds reconnection on a long-lived linkStateless, stable
Recommended forInternal tools, small teamsLarge services, Marketplace apps
Bolt setupSocketModeHandlerApp() + WSGI/ASGI

Socket Mode Runner Code

# Socket Mode (development, internal tools)
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

app = App(token=os.environ["SLACK_BOT_TOKEN"])
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()

HTTP Mode Runner Code (Flask)

# HTTP Mode (production, external services)
from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler
from flask import Flask, request

app = App(
    token=os.environ["SLACK_BOT_TOKEN"],
    signing_secret=os.environ["SLACK_SIGNING_SECRET"],
)
flask_app = Flask(__name__)
handler = SlackRequestHandler(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)

if __name__ == "__main__":
    flask_app.run(port=3000)

How to choose: Socket Mode is convenient during development or for an internal-only bot. If you plan to deploy to many workspaces or list the app on the Marketplace, use HTTP Mode.


8. Bot Framework Comparison -- Slack Bolt vs discord.py vs Telegram Bot

CategorySlack Bolt (Python)discord.pypython-telegram-bot
PlatformSlackDiscordTelegram
Installpip install slack-boltpip install discord.pypip install python-telegram-bot
AuthenticationOAuth 2.0 + Bot TokenBot TokenBot Token (BotFather)
Event deliverySocket Mode / HTTPGateway (WebSocket)Polling / Webhook
UI frameworkBlock Kit (rich blocks)Embed, Button, SelectInlineKeyboard, ReplyKeyboard
Modal supportSupported (views_open)Modal (discord.ui)Not supported (use a conversation flow)
Slash commandsNatively supportedApplication CommandBotCommand
File uploadfiles.upload APIFile objectsend_document
Rate limitsPer tier (1-100+ req/min)50 req/sec (global)30 msg/sec (group), 1/sec (individual)
Async supportAsyncApp providedAsync by defaultasyncio by default
Docs qualityExcellent (thorough official guide)Excellent (active community)Excellent (many examples)
EnterpriseSlack Enterprise GridLimitedLimited

9. Deployment Strategy -- Docker and Kubernetes

9.1 Dockerfile

FROM python:3.12-slim

WORKDIR /app

# copy the dependencies first (to use layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# copy the application code
COPY . .

# run as a non-root user
RUN adduser --disabled-password --gecos "" botuser
USER botuser

# health check (Socket Mode needs a separate HTTP server for this)
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD python -c "print('healthy')" || exit 1

CMD ["python", "app.py"]

9.2 Docker Compose

# docker-compose.yml
services:
  slack-bot:
    build: .
    env_file: .env
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '0.5'
    logging:
      driver: json-file
      options:
        max-size: '10m'
        max-file: '3'

9.3 Kubernetes Deployment

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: slack-bot
  labels:
    app: slack-bot
spec:
  replicas: 1 # a single instance is recommended for Socket Mode
  selector:
    matchLabels:
      app: slack-bot
  template:
    metadata:
      labels:
        app: slack-bot
    spec:
      containers:
        - name: slack-bot
          image: myregistry/slack-bot:latest
          resources:
            requests:
              memory: '128Mi'
              cpu: '100m'
            limits:
              memory: '256Mi'
              cpu: '500m'
          envFrom:
            - secretRef:
                name: slack-bot-secrets
          livenessProbe:
            exec:
              command:
                - python
                - -c
                - "print('alive')"
            initialDelaySeconds: 10
            periodSeconds: 30
          readinessProbe:
            exec:
              command:
                - python
                - -c
                - "print('ready')"
            initialDelaySeconds: 5
            periodSeconds: 10
      restartPolicy: Always
---
apiVersion: v1
kind: Secret
metadata:
  name: slack-bot-secrets
type: Opaque
stringData:
  SLACK_BOT_TOKEN: 'xoxb-your-bot-token'
  SLACK_APP_TOKEN: 'xapp-your-app-token'
  SLACK_SIGNING_SECRET: 'your-signing-secret'

A caution when deploying Socket Mode: Socket Mode holds only one WebSocket connection, so set replicas: 1. If you need high availability, switch to HTTP Mode and put a load balancer in front.


10. Error Handling and Rate Limit Management

10.1 Handling Rate Limits

The Slack API has a different rate limit tier for each method.

TierAllowanceRepresentative method
Tier 11 req/minadmin.*
Tier 220 req/minconversations.create
Tier 350 req/minchat.postMessage
Tier 4100+ req/minusers.info
Special1 req/sec (burst)chat.postMessage (per workspace)
# utils/rate_limiter.py
import time
import logging
from slack_sdk.errors import SlackApiError
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler

logger = logging.getLogger(__name__)


def configure_rate_limit_handler(client):
    """Add the rate limit handler to the WebClient"""
    rate_limit_handler = RateLimitErrorRetryHandler(max_retry_count=3)
    client.retry_handlers.append(rate_limit_handler)
    return client


def safe_api_call(func, *args, max_retries=3, **kwargs):
    """A safe API call wrapper that accounts for rate limits"""
    for attempt in range(max_retries):
        try:
            return func(*args, **kwargs)
        except SlackApiError as e:
            if e.response.status_code == 429:
                retry_after = int(e.response.headers.get("Retry-After", 5))
                logger.warning(
                    f"Rate limited. Retrying after {retry_after}s "
                    f"(attempt {attempt + 1}/{max_retries})"
                )
                time.sleep(retry_after)
            else:
                logger.error(f"Slack API error: {e.response['error']}")
                raise
    raise Exception(f"Max retries ({max_retries}) exceeded for API call")


# usage example
# result = safe_api_call(
#     client.chat_postMessage,
#     channel="#general",
#     text="Hello!"
# )

10.2 The Global Error Handling Pattern

# app-level error handler
@app.error
def global_error_handler(error, body, logger):
    """Catch unhandled errors across the whole app"""
    logger.exception(f"Unhandled error: {error}")

    # branch on the error type
    if isinstance(error, SlackApiError):
        error_code = error.response.get("error", "unknown_error")
        logger.error(f"Slack API Error: {error_code}")

        if error_code == "channel_not_found":
            logger.warning("Channel not found - check channel ID or bot permissions")
        elif error_code == "not_in_channel":
            logger.warning("Bot is not in the channel - invite the bot first")
        elif error_code == "token_revoked":
            logger.critical("Bot token has been revoked!")
    else:
        logger.error(f"Non-Slack error: {type(error).__name__}: {error}")

11. Failure Cases and Recovery Procedures

11.1 Common Failure Cases

Case 1: a timeout caused by not calling ack()

Case 2: the Socket Mode connection drops

Case 3: an infinite loop of bot messages

Case 4: modal input validation fails

Case 5: an expired token or insufficient scopes

11.2 Recovery Procedure Checklist

When an incident happens, work through the recovery in the following order.

  1. Check the logs: read the error message with docker logs slack-bot or kubectl logs
  2. Verify the token: call curl -H "Authorization: Bearer xoxb-..." https://slack.com/api/auth.test
  3. Check the event subscriptions: inspect the Event Subscriptions state on the Slack app settings page
  4. Check the network connection: for Socket Mode, the WebSocket connection state; for HTTP Mode, whether the endpoint is reachable
  5. Restart the process: docker restart slack-bot or kubectl rollout restart deployment/slack-bot
  6. Recheck the scopes: if the error message contains missing_scope, add that scope and reinstall the app

12. Operational Notes

Logging Recommendations

Security Cautions

Performance Optimization


13. Troubleshooting

SymptomCauseFix
"dispatch_failed" errorNo event handler is registeredCheck the handler registration, e.g. @app.event("message")
Slash command does not respondack() not called, or over 3 secondsAdd ack() on the first line of the handler
"not_authed" errorThe token is empty or wrongCheck the .env file and the environment variables
The modal does not opentrigger_id expired (3 seconds)Open the modal immediately after ack()
Reaction events never arriveThe Event Subscription is not configuredAdd a subscription for the reaction_added event
"missing_scope" errorA required OAuth scope is missingAdd the scope under OAuth & Permissions and reinstall
Socket Mode connection failsThe App Token was never created, or is wrongRegenerate the App Token under Basic Information
The bot cannot post to a channelThe bot has not joined that channelInvite the bot to the channel, or add the chat:write.public scope
HTTP 429 responseRate limit exceededWait as long as the Retry-After header says, then retry
Duplicate events receivedSlack's redelivery mechanismAdd idempotency logic keyed on the event ID

14. Production Deployment Checklist

Check every item below before deploying.


Conclusion

A Slack Bot is a tool that dramatically raises a team's efficiency. With the Python Bolt SDK you can implement every Slack interaction - event handling, slash commands, modals, Block Kit - in concise code.

The key points:


References

Comments

No comments yet.

Sign in to leave a comment