Split View: Slack Bot 개발 완벽 가이드 — Bolt SDK로 만드는 AI 업무 자동화 봇
Slack Bot 개발 완벽 가이드 — Bolt SDK로 만드는 AI 업무 자동화 봇

들어가며
Slack은 개발팀의 핵심 커뮤니케이션 플랫폼입니다. 잘 만든 Slack Bot은 장애 알림, 배포 관리, 코드 리뷰 리마인더, AI 질의 등 다양한 업무를 자동화합니다.
Bolt for Python은 Slack의 공식 프레임워크로, 간결한 API로 다양한 Slack 기능을 활용할 수 있습니다.
Slack 앱 설정
1. 앱 생성
1. https://api.slack.com/apps 접속
2. "Create New App" → "From scratch"
3. App Name: "DevOps Bot"
4. Workspace 선택
2. Bot Token Scopes 설정
OAuth & Permissions → Bot Token Scopes:
- chat:write (메시지 전송)
- commands (슬래시 명령어)
- im:history (DM 기록 읽기)
- im:read (DM 채널 정보)
- channels:history (채널 메시지 읽기)
- channels:read (채널 정보)
- users:read (사용자 정보)
- reactions:write (리액션 추가)
- files:write (파일 업로드)
3. Event Subscriptions
Event Subscriptions → Enable Events
Subscribe to bot events:
- message.im (DM 수신)
- message.channels (채널 메시지)
- app_mention (멘션)
- app_home_opened (앱 홈 탭 열기)
프로젝트 설정
# 의존성 설치
pip install slack-bolt slack-sdk python-dotenv openai
# 프로젝트 구조
slack-bot/
├── app.py # 메인 앱
├── handlers/
│ ├── __init__.py
│ ├── commands.py # 슬래시 명령어
│ ├── events.py # 이벤트 핸들러
│ ├── actions.py # 버튼/액션 핸들러
│ └── modals.py # 모달 뷰
├── services/
│ ├── __init__.py
│ ├── ai.py # LLM 통합
│ ├── deploy.py # 배포 서비스
│ └── github.py # GitHub 연동
├── .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
기본 앱 구성
# app.py
import os
from dotenv import load_dotenv
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
load_dotenv()
# Socket Mode로 앱 생성 (웹훅 서버 불필요)
app = App(token=os.environ["SLACK_BOT_TOKEN"])
# ===== 이벤트 핸들러 =====
# 멘션에 응답
@app.event("app_mention")
def handle_mention(event, say, client):
user = event["user"]
text = event["text"]
# 유저 정보 조회
user_info = client.users_info(user=user)
name = user_info["user"]["real_name"]
say(f"안녕하세요 {name}님! 무엇을 도와드릴까요? 🤖")
# DM 메시지 처리
@app.event("message")
def handle_dm(event, say, client, logger):
# 봇 자신의 메시지는 무시
if event.get("bot_id"):
return
channel_type = event.get("channel_type")
if channel_type == "im":
text = event.get("text", "")
say(f"말씀하신 내용을 처리하겠습니다: '{text}'")
# ===== 슬래시 명령어 =====
@app.command("/deploy")
def handle_deploy_command(ack, body, client):
ack() # 3초 내 응답 필수!
# 모달 열기
client.views_open(
trigger_id=body["trigger_id"],
view={
"type": "modal",
"callback_id": "deploy_modal",
"title": {"type": "plain_text", "text": "배포 관리"},
"submit": {"type": "plain_text", "text": "배포 시작"},
"blocks": [
{
"type": "input",
"block_id": "service_block",
"element": {
"type": "static_select",
"placeholder": {"type": "plain_text", "text": "서비스 선택"},
"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": "서비스"},
},
{
"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": "환경"},
},
{
"type": "input",
"block_id": "version_block",
"element": {
"type": "plain_text_input",
"placeholder": {"type": "plain_text", "text": "v1.2.3 또는 latest"},
"action_id": "version_input",
},
"label": {"type": "plain_text", "text": "버전"},
},
],
},
)
# 모달 제출 처리
@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"]
# 배포 알림 전송
client.chat_postMessage(
channel="#deployments",
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"🚀 *배포 시작*\n"
f"• 서비스: `{service}`\n"
f"• 환경: `{env}`\n"
f"• 버전: `{version}`\n"
f"• 요청자: <@{user_id}>"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "✅ 승인"},
"style": "primary",
"value": f"{service}|{env}|{version}",
"action_id": "deploy_approve",
},
{
"type": "button",
"text": {"type": "plain_text", "text": "❌ 거부"},
"style": "danger",
"value": f"{service}|{env}|{version}",
"action_id": "deploy_reject",
},
],
},
],
)
# 버튼 액션 처리
@app.action("deploy_approve")
def handle_approve(ack, body, client, action):
ack()
service, env, version = action["value"].split("|")
user_id = body["user"]["id"]
# 원래 메시지 업데이트
client.chat_update(
channel=body["channel"]["id"],
ts=body["message"]["ts"],
blocks=[
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"✅ *배포 승인됨*\n"
f"• 서비스: `{service}` → `{env}` ({version})\n"
f"• 승인자: <@{user_id}>\n"
f"• 상태: 배포 진행 중... ⏳"
}
}
],
)
# 실제 배포 로직 실행
# deploy_service(service, env, version)
# ===== 상태 조회 명령어 =====
@app.command("/status")
def handle_status(ack, say, command):
ack()
say(
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": "📊 서비스 상태"}
},
{
"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"마지막 업데이트: <!date^{int(__import__('time').time())}^{{date_short_pretty}} {{time}}|just now>"}
]
}
]
)
# ===== AI 챗봇 기능 =====
@app.command("/ask")
def handle_ask(ack, say, command):
ack()
question = command["text"]
if not question:
say("질문을 입력해주세요. 예: `/ask Kubernetes Pod이 CrashLoopBackOff일 때 대처법`")
return
# 로딩 표시
say(f"🤔 '{question}'에 대해 생각하고 있습니다...")
# 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 생성 답변 — 정확성을 확인하세요"}
]
}
]
)
# ===== 앱 홈 탭 =====
@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": "*사용 가능한 명령어:*\n"
"• `/deploy` — 서비스 배포\n"
"• `/status` — 서비스 상태 확인\n"
"• `/ask [질문]` — AI에게 질문\n"
"• `/incident` — 장애 보고"
}
},
{"type": "divider"},
{
"type": "section",
"text": {"type": "mrkdwn", "text": "*최근 배포:*\n✅ API Server v1.2.3 → Production (2시간 전)\n✅ Web v2.0.1 → Production (어제)"}
},
],
},
)
# ===== 스케줄 메시지 =====
def send_daily_standup_reminder():
"""매일 아침 스탠드업 리마인더"""
import time
app.client.chat_postMessage(
channel="#engineering",
text="🌅 *스탠드업 리마인더*\n오늘의 계획을 스레드에 공유해주세요!\n1. 어제 한 일\n2. 오늘 할 일\n3. 블로커",
)
# ===== 에러 핸들링 =====
@app.error
def handle_errors(error, body, logger):
logger.exception(f"Error: {error}")
logger.info(f"Request body: {body}")
# ===== 실행 =====
if __name__ == "__main__":
# Socket Mode (웹훅 서버 불필요, 방화벽 뒤에서도 동작)
handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
handler.start()
장애 관리 봇
@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": "🚨 장애 보고"},
"submit": {"type": "plain_text", "text": "보고"},
"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": "심각도"},
},
{
"type": "input",
"block_id": "description",
"element": {
"type": "plain_text_input",
"multiline": True,
"placeholder": {"type": "plain_text", "text": "장애 상황을 설명해주세요"},
"action_id": "desc_input",
},
"label": {"type": "plain_text", "text": "설명"},
},
],
},
)
@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"]
# 장애 채널 생성
import time
channel_name = f"incident-{int(time.time())}"
channel = client.conversations_create(
name=channel_name,
is_private=False,
)
channel_id = channel["channel"]["id"]
# 장애 초기 메시지
client.chat_postMessage(
channel=channel_id,
blocks=[
{
"type": "header",
"text": {"type": "plain_text", "text": f"🚨 장애 보고 ({severity})"}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*보고자:* <@{reporter}>\n"
f"*심각도:* {severity}\n"
f"*설명:*\n{description}"
}
},
{"type": "divider"},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "⏱ *타임라인을 이 스레드에 기록해주세요*"
}
},
],
)
# 알림 채널에 공지
client.chat_postMessage(
channel="#incidents",
text=f"🚨 새 장애 발생: {severity}\n📝 {description[:100]}...\n🔗 <#{channel_id}>",
)
Docker 배포
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
퀴즈
Q1. Slack Bolt에서 슬래시 명령어 핸들러의 첫 번째 해야 할 일은?
ack() 호출입니다. Slack은 3초 내에 응답(acknowledge)을 받지 못하면 타임아웃 에러를 표시합니다.
Q2. Socket Mode의 장점은?
WebSocket 연결을 사용하므로 공개 URL이나 웹훅 서버가 필요 없습니다. 방화벽 뒤에서도, 로컬 개발에서도 바로 동작합니다.
Q3. Block Kit의 주요 블록 타입 3가지는?
section (텍스트/필드), actions (버튼/셀렉트 등 인터랙티브 요소), input (모달에서 사용자 입력)입니다.
Q4. @app.action과 @app.view의 차이는?
@app.action은 버튼 클릭 등의 인터랙션을, @app.view는 모달 제출(submit)을 처리합니다. 각각 action_id와 callback_id로 매칭됩니다.
Q5. Bot Token(xoxb-)과 App Token(xapp-)의 차이는?
Bot Token은 채널 메시지 전송 등 Slack API 호출에 사용하고, App Token은 Socket Mode WebSocket 연결에 사용합니다.
Q6. 장애 발생 시 자동으로 Slack 채널을 만드는 이유는?
장애 관련 소통을 한 곳에 집중시키고, 타임라인을 기록하며, 사후 분석(포스트모템)을 위한 이력을 남기기 위해서입니다.
Q7. Slack Bot에 LLM을 통합할 때 주의할 점은?
LLM 응답 시간이 3초를 넘을 수 있으므로 먼저 ack()로 응답한 후, 비동기로 LLM을 호출하고 결과를 별도 메시지로 전송해야 합니다.
마무리
Slack Bot은 팀의 생산성을 크게 높이는 도구입니다. Bolt SDK를 사용하면 슬래시 명령어, 모달, 버튼 인터랙션 등을 간결한 Python 코드로 구현할 수 있고, Socket Mode로 별도 서버 없이도 바로 배포할 수 있습니다.
참고 자료
Complete Guide to Slack Bot Development — Building an AI Task Automation Bot with Bolt SDK
- 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.