🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

Build an AI Slack Incident Classifier with Claude API

Build a Python Slack bot that reads incident messages, classifies them by severity (P1/P2/P3), affected service, and type using Claude Haiku, then posts structured summaries to a dedicated channel.

DevOpsBoys5 min read
Share:Tweet

Your on-call engineer is asleep. A flood of Slack messages hits #incidents. By the time someone reads through 40 messages, 10 minutes have passed. This bot reads every message in your incidents channel, classifies severity and type using Claude Haiku, and posts a clean structured summary to #incidents-summary — automatically.

What You Are Building

  • A Slack Bolt app (Python) listening to messages in #incidents
  • Each message gets classified by claude-haiku-4-5 for:
    • Severity: P1 (customer-facing outage), P2 (degraded service), P3 (minor / no customer impact)
    • Affected service: extracted from the message
    • Incident type: infra, app, security, or network
  • A structured summary is posted to #incidents-summary
  • Deployable as a Lambda function or Docker container

Prerequisites

bash
pip install slack-bolt anthropic python-dotenv

Create a Slack app at api.slack.com/apps with these scopes:

  • channels:history — read messages
  • chat:write — post to channels
  • channels:read — list channels

Enable Socket Mode (for local dev) or Events API (for production). Subscribe to the message.channels event.

Environment Variables

bash
# .env
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_APP_TOKEN=xapp-your-app-token   # only for Socket Mode
ANTHROPIC_API_KEY=sk-ant-your-key
INCIDENTS_CHANNEL_ID=C0123456789
SUMMARY_CHANNEL_ID=C9876543210

The Classification Prompt

The prompt is the core of the classifier. We ask Claude to return structured JSON so parsing is deterministic:

python
CLASSIFICATION_PROMPT = """You are an incident classifier for a DevOps team.
 
Analyze this Slack message from an incidents channel and return a JSON object with exactly these fields:
- severity: "P1", "P2", or "P3"
  P1 = customer-facing outage or data loss, needs immediate response
  P2 = degraded performance or partial outage, affects customers but workaround exists
  P3 = internal issue, no customer impact, can wait until business hours
- service: the affected service/system name (string, "unknown" if not mentioned)
- incident_type: one of "infra", "app", "security", "network"
  infra = servers, clusters, nodes, databases down
  app = application error, deployment failure, bug in production
  security = auth failure, unusual access, credential leak, vulnerability
  network = DNS, routing, load balancer, connectivity issues
- summary: one sentence describing the incident (max 120 chars)
- confidence: "high", "medium", or "low" (how confident you are in this classification)
 
Return ONLY valid JSON. No explanation. No markdown.
 
Message: {message}"""

Full Application Code

python
import json
import os
import logging
from dotenv import load_dotenv
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler
import anthropic
 
load_dotenv()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
 
app = App(token=os.environ["SLACK_BOT_TOKEN"])
claude = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
 
INCIDENTS_CHANNEL = os.environ["INCIDENTS_CHANNEL_ID"]
SUMMARY_CHANNEL = os.environ["SUMMARY_CHANNEL_ID"]
 
SEVERITY_EMOJI = {"P1": ":red_circle:", "P2": ":yellow_circle:", "P3": ":white_circle:"}
TYPE_EMOJI = {"infra": ":computer:", "app": ":bug:", "security": ":lock:", "network": ":globe_with_meridians:"}
 
 
def classify_incident(message_text: str) -> dict:
    prompt = CLASSIFICATION_PROMPT.format(message=message_text)
 
    response = claude.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=256,
        messages=[{"role": "user", "content": prompt}],
    )
 
    raw = response.content[0].text.strip()
    return json.loads(raw)
 
 
def format_summary_block(classification: dict, original_text: str, user: str, channel: str) -> list:
    severity = classification.get("severity", "P3")
    service = classification.get("service", "unknown")
    inc_type = classification.get("incident_type", "app")
    summary = classification.get("summary", original_text[:120])
    confidence = classification.get("confidence", "medium")
 
    sev_emoji = SEVERITY_EMOJI.get(severity, ":white_circle:")
    type_emoji = TYPE_EMOJI.get(inc_type, ":bell:")
 
    return [
        {
            "type": "header",
            "text": {"type": "plain_text", "text": f"{sev_emoji} {severity} Incident Detected"},
        },
        {
            "type": "section",
            "fields": [
                {"type": "mrkdwn", "text": f"*Service:*\n{service}"},
                {"type": "mrkdwn", "text": f"*Type:*\n{type_emoji} {inc_type}"},
                {"type": "mrkdwn", "text": f"*Reported by:*\n<@{user}>"},
                {"type": "mrkdwn", "text": f"*Channel:*\n<#{channel}>"},
            ],
        },
        {"type": "section", "text": {"type": "mrkdwn", "text": f"*Summary:*\n{summary}"}},
        {
            "type": "context",
            "elements": [{"type": "mrkdwn", "text": f"Classification confidence: {confidence} | Model: claude-haiku-4-5"}],
        },
        {"type": "divider"},
    ]
 
 
@app.event("message")
def handle_message(event, say, client):
    channel_id = event.get("channel")
    text = event.get("text", "")
    user = event.get("user", "unknown")
    subtype = event.get("subtype")
 
    # Only process messages from the incidents channel, skip bot messages
    if channel_id != INCIDENTS_CHANNEL or subtype == "bot_message" or not text:
        return
 
    # Skip very short messages (reactions, acks)
    if len(text) < 20:
        return
 
    logger.info(f"Classifying message from {user}: {text[:80]}...")
 
    try:
        classification = classify_incident(text)
        blocks = format_summary_block(classification, text, user, channel_id)
 
        client.chat_postMessage(
            channel=SUMMARY_CHANNEL,
            blocks=blocks,
            text=f"{classification.get('severity', 'P3')} incident: {classification.get('summary', text[:80])}",
        )
 
        logger.info(f"Posted classification: {classification}")
 
    except json.JSONDecodeError as e:
        logger.error(f"Claude returned invalid JSON: {e}")
    except Exception as e:
        logger.error(f"Classification failed: {e}")
 
 
if __name__ == "__main__":
    handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
    handler.start()

Deploying as a Docker Container

dockerfile
FROM python:3.12-slim
 
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
 
COPY . .
 
CMD ["python", "app.py"]

For production, switch from Socket Mode to the HTTP Events API. Use a load balancer or API Gateway to receive Slack events and forward them to your container.

Deploying as AWS Lambda

For serverless deployment, use the Slack Bolt Lambda adapter:

bash
pip install slack-bolt[aws-lambda]
python
from slack_bolt.adapter.aws_lambda import SlackRequestHandler
 
def lambda_handler(event, context):
    slack_handler = SlackRequestHandler(app=app)
    return slack_handler.handle(event, context)

Deploy with a Function URL or API Gateway. Set the Slack Events API Request URL to your Lambda endpoint. Store secrets in AWS Secrets Manager and load them at cold start.

Cost Estimate

Claude Haiku is extremely cheap. At roughly $0.80 per million input tokens and $4 per million output tokens:

  • Average incident message: ~100 tokens in + ~80 tokens out
  • 1,000 classified incidents/month: ~$0.10 in + ~$0.32 out = $0.42/month total

This is effectively free. You could process 50,000 incidents per month for under $25.

What to Build Next

  • Add a PostgreSQL or DynamoDB table to store classifications and build incident trend reports
  • Trigger PagerDuty or OpsGenie alerts automatically for P1 detections
  • Build a weekly report that summarizes incident counts by type and service
  • Add a /incidents summary slash command that queries the last 24 hours of classifications

The classifier as built handles the core loop: read message → classify → post summary. Every extension from here is just adding more outputs to that same pipeline.

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments