- Authors
- Name
- Introduction
- Slack App Setup
- Project Setup
- Basic App Configuration
- Incident Management Bot
- Docker Deployment
- Quiz
- Conclusion
- References

Introduction
Slack is a core communication platform for development teams. A well-built Slack Bot can automate various tasks such as incident alerts, deployment management, code review reminders, and AI queries.
Bolt for Python is Slack's official framework that provides a concise API for leveraging various Slack features.
Slack App Setup
1. Create an App
1. Go to https://api.slack.com/apps
2. "Create New App" → "From scratch"
3. App Name: "DevOps Bot"
4. Select Workspace
2. Bot Token Scopes Configuration
OAuth & Permissions → Bot Token Scopes:
- chat:write (Send messages)
- commands (Slash commands)
- im:history (Read DM history)
- im:read (DM channel info)
- channels:history (Read channel messages)
- channels:read (Channel info)
- users:read (User info)
- reactions:write (Add reactions)
- files:write (Upload files)
3. Event Subscriptions
Event Subscriptions → Enable Events
Subscribe to bot events:
- message.im (Receive DMs)
- message.channels (Channel messages)
- app_mention (Mentions)
- app_home_opened (App Home tab opened)
Project Setup
# Install dependencies
pip install slack-bolt slack-sdk python-dotenv openai
# Project structure
slack-bot/
├── app.py # Main app
├── handlers/
│ ├── __init__.py
│ ├── commands.py # Slash commands
│ ├── events.py # Event handlers
│ ├── actions.py # Button/action handlers
│ └── modals.py # Modal views
├── services/
│ ├── __init__.py
│ ├── ai.py # LLM integration
│ ├── deploy.py # Deployment service
│ └── github.py # GitHub integration
├── .env
└── requirements.txt
# .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-key
Basic App Configuration
# app.py
import os
from dotenv import load_dotenv
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
load_dotenv()
# Create app with Socket Mode (no webhook server required)
app = App(token=os.environ["SLACK_BOT_TOKEN"])
# ===== Event Handlers =====
# Respond to mentions
@app.event("app_mention")
def handle_mention(event, say, client):
user = event["user"]
text = event["text"]
# Look up user info
user_info = client.users_info(user=user)
name = user_info["user"]["real_name"]
say(f"Hello {name}! How can I help you? 🤖")
# Handle DM messages
@app.event("message")
def handle_dm(event, say, client, logger):
# Ignore bot's own messages
if event.get("bot_id"):
return
channel_type = event.get("channel_type")
if channel_type == "im":
text = event.get("text", "")
say(f"I'll process your request: '{text}'")
# ===== Slash Commands =====
@app.command("/deploy")
def handle_deploy_command(ack, body, client):
ack() # Must respond within 3 seconds!
# Open modal
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "deploy_modal",
"title": {"type": "plain_text", "text": "Deployment Manager"},
"submit": {"type": "plain_text", "text": "Start Deploy"},
"blocks": [
{
"type": "input",
"block_id": "service_block",
"element": {
"type": "static_select",
"placeholder": {"type": "plain_text", "text": "Select service"},
"options": [
{"text": {"type": "plain_text", "text": "API Server"}, "value": "api"},
{"text": {"type": "plain_text", "text": "Web Frontend"}, "value": "web"},
{"text": {"type": "plain_text", "text": "Worker"}, "value": "worker"},
],
"action_id": "service_select",
},
"label": {"type": "plain_text", "text": "Service"},
},
{
"type": "input",
"block_id": "env_block",
"element": {
"type": "static_select",
"options": [
{"text": {"type": "plain_text", "text": "Staging"}, "value": "staging"},
{"text": {"type": "plain_text", "text": "Production"}, "value": "production"},
],
"action_id": "env_select",
},
"label": {"type": "plain_text", "text": "Environment"},
},
{
"type": "input",
"block_id": "version_block",
"element": {
"type": "plain_text_input",
"placeholder": {"type": "plain_text", "text": "v1.2.3 or latest"},
"action_id": "version_input",
},
"label": {"type": "plain_text", "text": "Version"},
},
],
},
)
# Handle modal submission
@app.view("deploy_modal")
def handle_deploy_submission(ack, body, client, view):
ack()
values = view["state"]["values"]
service = values["service_block"]["service_select"]["selected_option"]["value"]
env = values["env_block"]["env_select"]["selected_option"]["value"]
version = values["version_block"]["version_input"]["value"]
user_id = body["user"]["id"]
# Send deployment notification
client.chat_postMessage(
channel="#deployments",
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"🚀 *Deployment Started*\n"
f"• Service: `{service}`\n"
f"• Environment: `{env}`\n"
f"• Version: `{version}`\n"
f"• Requester: <@{user_id}>"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "✅ Approve"},
"style": "primary",
"value": f"{service}|{env}|{version}",
"action_id": "deploy_approve",
},
{
"type": "button",
"text": {"type": "plain_text", "text": "❌ Reject"},
"style": "danger",
"value": f"{service}|{env}|{version}",
"action_id": "deploy_reject",
},
],
},
],
)
# Handle button actions
@app.action("deploy_approve")
def handle_approve(ack, body, client, action):
ack()
service, env, version = action["value"].split("|")
user_id = body["user"]["id"]
# Update original message
client.chat_update(
channel=body["channel"]["id"],
ts=body["message"]["ts"],
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"✅ *Deployment Approved*\n"
f"• Service: `{service}` → `{env}` ({version})\n"
f"• Approver: <@{user_id}>\n"
f"• Status: Deploying... ⏳"
}
}
],
)
# Execute actual deployment logic
# deploy_service(service, env, version)
# ===== Status Query Command =====
@app.command("/status")
def handle_status(ack, say, command):
ack()
say(
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": "📊 Service Status"}
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": "*API Server*\n🟢 Healthy (v1.2.3)"},
{"type": "mrkdwn", "text": "*Web Frontend*\n🟢 Healthy (v2.0.1)"},
{"type": "mrkdwn", "text": "*Worker*\n🟡 Degraded (v1.1.0)"},
{"type": "mrkdwn", "text": "*Database*\n🟢 Healthy (CPU: 45%)"},
]
},
{"type": "divider"},
{
"type": "context",
"elements": [
{"type": "mrkdwn", "text": f"Last updated: <!date^{int(__import__('time').time())}^{{date_short_pretty}} {{time}}|just now>"}
]
}
]
)
# ===== AI Chatbot Feature =====
@app.command("/ask")
def handle_ask(ack, say, command):
ack()
question = command["text"]
if not question:
say("Please enter a question. Example: `/ask How to handle CrashLoopBackOff in Kubernetes Pods`")
return
# Show loading indicator
say(f"🤔 Thinking about '{question}'...")
# Call LLM
from openai import OpenAI
client_ai = OpenAI()
response = client_ai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "You are a helpful DevOps assistant. Answer in Korean. Be concise."},
{"role": "user", "content": question}
],
max_tokens=1000,
)
answer = response.choices[0].message.content
say(
blocks=[
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"*Q: {question}*"}
},
{"type": "divider"},
{
"type": "section",
"text": {"type": "mrkdwn", "text": answer}
},
{
"type": "context",
"elements": [
{"type": "mrkdwn", "text": "💡 AI-generated answer — please verify accuracy"}
]
}
]
)
# ===== App Home Tab =====
@app.event("app_home_opened")
def update_home_tab(client, event):
client.views_publish(
user_id=event["user"],
view={
"type": "home",
"blocks": [
{
"type": "header",
"text": {"type": "plain_text", "text": "🤖 DevOps Bot Dashboard"}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Available commands:*\n"
"• `/deploy` — Deploy a service\n"
"• `/status` — Check service status\n"
"• `/ask [question]` — Ask AI\n"
"• `/incident` — Report an incident"
}
},
{"type": "divider"},
{
"type": "section",
"text": {"type": "mrkdwn", "text": "*Recent deployments:*\n✅ API Server v1.2.3 → Production (2 hours ago)\n✅ Web v2.0.1 → Production (yesterday)"}
},
],
},
)
# ===== Scheduled Messages =====
def send_daily_standup_reminder():
"""Daily morning standup reminder"""
import time
app.client.chat_postMessage(
channel="#engineering",
text="🌅 *Standup Reminder*\nPlease share your daily plan in the thread!\n1. What you did yesterday\n2. What you'll do today\n3. Blockers",
)
# ===== Error Handling =====
@app.error
def handle_errors(error, body, logger):
logger.exception(f"Error: {error}")
logger.info(f"Request body: {body}")
# ===== Run =====
if __name__ == "__main__":
# Socket Mode (no webhook server needed, works behind firewalls)
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()
Incident Management Bot
@app.command("/incident")
def handle_incident(ack, body, client):
ack()
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "incident_modal",
"title": {"type": "plain_text", "text": "🚨 Incident Report"},
"submit": {"type": "plain_text", "text": "Report"},
"blocks": [
{
"type": "input",
"block_id": "severity",
"element": {
"type": "static_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"},
],
"action_id": "severity_select",
},
"label": {"type": "plain_text", "text": "Severity"},
},
{
"type": "input",
"block_id": "description",
"element": {
"type": "plain_text_input",
"multiline": True,
"placeholder": {"type": "plain_text", "text": "Describe the incident"},
"action_id": "desc_input",
},
"label": {"type": "plain_text", "text": "Description"},
},
],
},
)
@app.view("incident_modal")
def handle_incident_submit(ack, body, client, view):
ack()
values = view["state"]["values"]
severity = values["severity"]["severity_select"]["selected_option"]["value"]
description = values["description"]["desc_input"]["value"]
reporter = body["user"]["id"]
# Create incident channel
import time
channel_name = f"incident-{int(time.time())}"
channel = client.conversations_create(
name=channel_name,
is_private=False,
)
channel_id = channel["channel"]["id"]
# Initial incident message
client.chat_postMessage(
channel=channel_id,
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": f"🚨 Incident Report ({severity})"}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*Reporter:* <@{reporter}>\n"
f"*Severity:* {severity}\n"
f"*Description:*\n{description}"
}
},
{"type": "divider"},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "⏱ *Please record the timeline in this thread*"
}
},
],
)
# Announce in notification channel
client.chat_postMessage(
channel="#incidents",
text=f"🚨 New incident: {severity}\n📝 {description[:100]}...\n🔗 <#{channel_id}>",
)
Docker Deployment
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
# docker-compose.yml
version: '3.8'
services:
slack-bot:
build: .
env_file: .env
restart: unless-stopped
healthcheck:
test: ['CMD', 'python', '-c', "import requests; requests.get('http://localhost:3000/health')"]
interval: 30s
timeout: 10s
retries: 3
Quiz
Q1. What is the first thing a slash command handler must do in Slack Bolt?
Call ack(). Slack displays a timeout error if it doesn't receive an acknowledgment within 3 seconds.
Q2. What is the advantage of Socket Mode?
It uses WebSocket connections, so no public URL or webhook server is needed. It works behind firewalls and in local development environments immediately.
Q3. What are the 3 main block types in Block Kit?
section (text/fields), actions (interactive elements like buttons/selects), and input (user input in modals).
Q4. What is the difference between @app.action and @app.view?
@app.action handles interactions like button clicks, while @app.view handles modal submissions. They match using action_id and callback_id respectively.
Q5. What is the difference between Bot Token (xoxb-) and App Token (xapp-)?
The Bot Token is used for Slack API calls such as sending channel messages, while the App Token is used for Socket Mode WebSocket connections.
Q6. Why automatically create a Slack channel when an incident occurs?
To centralize incident-related communication in one place, record a timeline, and maintain a history for post-incident analysis (postmortem).
Q7. What should you be cautious about when integrating an LLM with a Slack Bot?
LLM response times may exceed 3 seconds, so you should first respond with ack(), then call the LLM asynchronously and send the result as a separate message.
Conclusion
Slack Bots are tools that significantly boost team productivity. With the Bolt SDK, you can implement slash commands, modals, and button interactions in concise Python code, and deploy immediately without a separate server using Socket Mode.