NYC

slack-api

SKILL.md

Slack API Skill

Master Slack bot development and workspace automation using the Slack Platform. This skill covers the Web API, Events API, Socket Mode, Block Kit UI framework, and the Python Bolt SDK for building production-ready Slack applications.

When to Use This Skill

USE when:

  • Building notification systems for CI/CD pipelines
  • Creating interactive bots for team workflows
  • Automating incident response and alerting
  • Building approval workflows with interactive messages
  • Integrating external services with Slack channels
  • Creating slash commands for common operations
  • Building internal tools with modal dialogs
  • Implementing scheduled message automation

DON'T USE when:

  • Microsoft Teams is the primary platform (use teams-api)
  • Simple one-way notifications only (use incoming webhooks directly)
  • Need email-based workflows (different domain)
  • Slack Enterprise Grid with complex org requirements
  • Real-time gaming or high-frequency updates (consider WebSockets)

Prerequisites

Slack App Setup

# 1. Create a Slack App at https://api.slack.com/apps
# 2. Choose "From scratch" and select your workspace

# Required Bot Token Scopes (OAuth & Permissions):
# - chat:write          - Post messages
# - chat:write.public   - Post to channels without joining
# - channels:read       - List public channels
# - channels:history    - Read channel messages
# - groups:read         - List private channels
# - im:read             - List direct messages
# - users:read          - Access user information
# - files:write         - Upload files
# - reactions:write     - Add reactions
# - commands            - Add slash commands

# Event Subscriptions (for Events API):
# - message.channels    - Messages in public channels
# - message.groups      - Messages in private channels
# - message.im          - Direct messages
# - app_mention         - When bot is mentioned

# Interactive Components:
# - Enable in app settings
# - Set Request URL for button/select handling

Python Environment Setup

# Create virtual environment
python -m venv slack-bot-env
source slack-bot-env/bin/activate  # Linux/macOS
# slack-bot-env\Scripts\activate   # Windows

# Install Slack Bolt SDK
pip install slack-bolt slack-sdk

# Install additional dependencies
pip install python-dotenv aiohttp requests

# Create requirements.txt
cat > requirements.txt << 'EOF'
slack-bolt>=1.18.0
slack-sdk>=3.21.0
python-dotenv>=1.0.0
aiohttp>=3.9.0
requests>=2.31.0
EOF

# Environment variables
cat > .env << 'EOF'
SLACK_BOT_TOKEN=xoxb-your-bot-token
SLACK_SIGNING_SECRET=your-signing-secret
SLACK_APP_TOKEN=xapp-your-app-token  # For Socket Mode
EOF

Local Development with ngrok

# Install ngrok
brew install ngrok  # macOS
# Or download from https://ngrok.com/download

# Authenticate ngrok
ngrok config add-authtoken YOUR_AUTH_TOKEN

# Start tunnel for local development
ngrok http 3000

# Use the HTTPS URL for:
# - Event Subscriptions Request URL
# - Interactive Components Request URL
# - Slash Commands Request URL

Core Capabilities

1. Basic Slack Bot with Bolt

# app.py
# ABOUTME: Basic Slack bot using Bolt framework
# ABOUTME: Handles messages, mentions, and slash commands

import os
from dotenv import load_dotenv
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

load_dotenv()

# Initialize app with bot token and signing secret
app = App(
    token=os.environ.get("SLACK_BOT_TOKEN"),
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET")
)

# Listen for messages containing "hello"
@app.message("hello")
def message_hello(message, say):
    """Respond to messages containing 'hello'"""
    user = message['user']
    say(f"Hey there <@{user}>!")

# Listen for app mentions
@app.event("app_mention")
def handle_app_mention(event, say, client):
    """Respond when bot is mentioned"""
    user = event['user']
    channel = event['channel']
    text = event['text']

    # Get user info
    user_info = client.users_info(user=user)
    user_name = user_info['user']['real_name']

    say(f"Hi {user_name}! You mentioned me with: {text}")

# Handle message events
@app.event("message")
def handle_message_events(body, logger):
    """Log all message events"""
    logger.info(f"Message event: {body}")

# Slash command handler
@app.command("/greet")
def handle_greet_command(ack, say, command):
    """Handle /greet slash command"""
    ack()  # Acknowledge command within 3 seconds

    user = command['user_id']
    text = command.get('text', 'everyone')

    say(f"<@{user}> sends greetings to {text}!")

# Error handler
@app.error
def custom_error_handler(error, body, logger):
    """Handle errors gracefully"""
    logger.exception(f"Error: {error}")
    logger.info(f"Request body: {body}")

# Run with Socket Mode (no public URL needed)
if __name__ == "__main__":
    handler = SocketModeHandler(
        app,
        os.environ.get("SLACK_APP_TOKEN")
    )
    print("Bot is running...")
    handler.start()

2. Block Kit Messages

# blocks.py
# ABOUTME: Block Kit message construction utilities
# ABOUTME: Creates rich, interactive Slack messages

from slack_bolt import App
import os

app = App(token=os.environ.get("SLACK_BOT_TOKEN"))

def create_deployment_message(
    environment: str,
    version: str,
    status: str,
    deploy_url: str,
    logs_url: str
) -> list:
    """Create a deployment notification with Block Kit"""

    status_emoji = {
        "success": ":white_check_mark:",
        "failure": ":x:",
        "in_progress": ":hourglass_flowing_sand:",
        "pending": ":clock3:"
    }

    emoji = status_emoji.get(status, ":question:")

    blocks = [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": f"{emoji} Deployment {status.title()}",
                "emoji": True
            }
        },
        {
            "type": "section",
            "fields": [
                {
                    "type": "mrkdwn",
                    "text": f"*Environment:*\n{environment}"
                },
                {
                    "type": "mrkdwn",
                    "text": f"*Version:*\n{version}"
                },
                {
                    "type": "mrkdwn",
                    "text": f"*Status:*\n{status.title()}"
                },
                {
                    "type": "mrkdwn",
                    "text": f"*Time:*\n<!date^{int(time.time())}^{{date_short}} at {{time}}|now>"
                }
            ]
        },
        {
            "type": "divider"
        },
        {
            "type": "actions",
            "elements": [
                {
                    "type": "button",
                    "text": {
                        "type": "plain_text",
                        "text": "View Deployment",
                        "emoji": True
                    },
                    "url": deploy_url,
                    "style": "primary"
                },
                {
                    "type": "button",
                    "text": {
                        "type": "plain_text",
                        "text": "View Logs",
                        "emoji": True
                    },
                    "url": logs_url
                }
            ]
        },
        {
            "type": "context",
            "elements": [
                {
                    "type": "mrkdwn",
                    "text": "Deployed by CI/CD Pipeline"
                }
            ]
        }
    ]

    return blocks

def create_approval_message(
    request_id: str,
    requester: str,
    description: str,
    details: dict
) -> list:
    """Create an approval request with interactive buttons"""

    blocks = [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": ":clipboard: Approval Request",
                "emoji": True
            }
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Request ID:* `{request_id}`\n*Requested by:* <@{requester}>\n\n{description}"
            }
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "*Details:*\n" + "\n".join(
                    f"- {k}: {v}" for k, v in details.items()
                )
            }
        },
        {
            "type": "divider"
        },
        {
            "type": "actions",
            "block_id": f"approval_{request_id}",
            "elements": [
                {
                    "type": "button",
                    "text": {
                        "type": "plain_text",
                        "text": "Approve",
                        "emoji": True
                    },
                    "style": "primary",
                    "action_id": "approve_request",
                    "value": request_id
                },
                {
                    "type": "button",
                    "text": {
                        "type": "plain_text",
                        "text": "Reject",
                        "emoji": True
                    },
                    "style": "danger",
                    "action_id": "reject_request",
                    "value": request_id
                },
                {
                    "type": "button",
                    "text": {
                        "type": "plain_text",
                        "text": "Request Info",
                        "emoji": True
                    },
                    "action_id": "request_info",
                    "value": request_id
                }
            ]
        }
    ]

    return blocks

def create_poll_message(question: str, options: list) -> list:
    """Create a poll with radio buttons"""

    option_elements = [
        {
            "text": {
                "type": "plain_text",
                "text": option,
                "emoji": True
            },
            "value": f"option_{i}"
        }
        for i, option in enumerate(options)
    ]

    blocks = [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": ":bar_chart: Poll",
                "emoji": True
            }
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*{question}*"
            }
        },
        {
            "type": "divider"
        },
        {
            "type": "section",
            "block_id": "poll_options",
            "text": {
                "type": "mrkdwn",
                "text": "Select your choice:"
            },
            "accessory": {
                "type": "radio_buttons",
                "action_id": "poll_vote",
                "options": option_elements
            }
        },
        {
            "type": "actions",
            "elements": [
                {
                    "type": "button",
                    "text": {
                        "type": "plain_text",
                        "text": "Submit Vote",
                        "emoji": True
                    },
                    "style": "primary",
                    "action_id": "submit_vote"
                }
            ]
        }
    ]

    return blocks

# Send deployment notification
def send_deployment_notification(channel: str):
    """Send a deployment notification to a channel"""

    blocks = create_deployment_message(
        environment="production",
        version="v2.1.0",
        status="success",
        deploy_url="https://app.example.com",
        logs_url="https://logs.example.com"
    )

    app.client.chat_postMessage(
        channel=channel,
        blocks=blocks,
        text="Deployment notification"  # Fallback for notifications
    )

3. Interactive Components and Actions

# interactive.py
# ABOUTME: Handle interactive components like buttons, selects, modals
# ABOUTME: Implements approval workflows with state management

from slack_bolt import App
from datetime import datetime
import json
import os

app = App(token=os.environ.get("SLACK_BOT_TOKEN"))

# In-memory storage (use database in production)
approval_requests = {}

@app.action("approve_request")
def handle_approve(ack, body, client, logger):
    """Handle approval button click"""
    ack()

    user = body['user']['id']
    request_id = body['actions'][0]['value']
    channel = body['channel']['id']
    message_ts = body['message']['ts']

    # Update the message to show approval
    updated_blocks = body['message']['blocks'].copy()

    # Remove action buttons
    updated_blocks = [b for b in updated_blocks if b.get('type') != 'actions']

    # Add approval status
    updated_blocks.append({
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": f":white_check_mark: *Approved* by <@{user}> at {datetime.now().strftime('%Y-%m-%d %H:%M')}"
        }
    })

    # Update the original message
    client.chat_update(
        channel=channel,
        ts=message_ts,
        blocks=updated_blocks,
        text="Request approved"
    )

    # Store approval
    approval_requests[request_id] = {
        "status": "approved",
        "approved_by": user,
        "timestamp": datetime.now().isoformat()
    }

    logger.info(f"Request {request_id} approved by {user}")

@app.action("reject_request")
def handle_reject(ack, body, client, respond):
    """Handle rejection with reason modal"""
    ack()

    request_id = body['actions'][0]['value']
    trigger_id = body['trigger_id']

    # Open modal for rejection reason
    client.views_open(
        trigger_id=trigger_id,
        view={
            "type": "modal",
            "callback_id": f"reject_modal_{request_id}",
            "title": {
                "type": "plain_text",
                "text": "Reject Request"
            },
            "submit": {
                "type": "plain_text",
                "text": "Reject"
            },
            "close": {
                "type": "plain_text",
                "text": "Cancel"
            },
            "blocks": [
                {
                    "type": "input",
                    "block_id": "reason_block",
                    "element": {
                        "type": "plain_text_input",
                        "action_id": "rejection_reason",
                        "multiline": True,
                        "placeholder": {
                            "type": "plain_text",
                            "text": "Enter reason for rejection..."
                        }
                    },
                    "label": {
                        "type": "plain_text",
                        "text": "Rejection Reason"
                    }
                }
            ],
            "private_metadata": json.dumps({
                "channel": body['channel']['id'],
                "message_ts": body['message']['ts'],
                "request_id": request_id
            })
        }
    )

@app.view_submission("reject_modal_.*")
def handle_reject_submission(ack, body, client, view, logger):
    """Handle rejection modal submission"""
    ack()

    # Get values from modal
    reason = view['state']['values']['reason_block']['rejection_reason']['value']
    metadata = json.loads(view['private_metadata'])
    user = body['user']['id']

    channel = metadata['channel']
    message_ts = metadata['message_ts']
    request_id = metadata['request_id']

    # Update original message
    client.chat_postMessage(
        channel=channel,
        thread_ts=message_ts,
        text=f":x: *Rejected* by <@{user}>\n*Reason:* {reason}"
    )

    # Update the original message blocks
    original_message = client.conversations_history(
        channel=channel,
        latest=message_ts,
        inclusive=True,
        limit=1
    )

    if original_message['messages']:
        updated_blocks = original_message['messages'][0].get('blocks', [])
        updated_blocks = [b for b in updated_blocks if b.get('type') != 'actions']
        updated_blocks.append({
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f":x: *Rejected* by <@{user}>"
            }
        })

        client.chat_update(
            channel=channel,
            ts=message_ts,
            blocks=updated_blocks,
            text="Request rejected"
        )

    logger.info(f"Request {request_id} rejected by {user}: {reason}")

@app.action("poll_vote")
def handle_poll_vote(ack, body, logger):
    """Handle poll vote selection"""
    ack()
    selected = body['actions'][0]['selected_option']['value']
    logger.info(f"Poll vote: {selected}")

@app.action("submit_vote")
def handle_submit_vote(ack, body, client, respond):
    """Handle poll submission"""
    ack()

    user = body['user']['id']

    # Get selected option from state
    state = body.get('state', {}).get('values', {})
    selected = None

    for block_id, block_values in state.items():
        for action_id, action_value in block_values.items():
            if action_value.get('selected_option'):
                selected = action_value['selected_option']

    if selected:
        respond(
            text=f"<@{user}> voted for: {selected['text']['text']}",
            response_type="in_channel"
        )
    else:
        respond(
            text="Please select an option before submitting.",
            response_type="ephemeral"
        )

4. Modals and Views

# modals.py
# ABOUTME: Modal dialogs for complex user input
# ABOUTME: Multi-step workflows with view updates

from slack_bolt import App
import json
import os

app = App(token=os.environ.get("SLACK_BOT_TOKEN"))

@app.command("/create-ticket")
def open_ticket_modal(ack, body, client):
    """Open a modal for ticket creation"""
    ack()

    client.views_open(
        trigger_id=body['trigger_id'],
        view={
            "type": "modal",
            "callback_id": "create_ticket_modal",
            "title": {
                "type": "plain_text",
                "text": "Create Ticket"
            },
            "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": "Brief description of the issue"
                        }
                    },
                    "label": {
                        "type": "plain_text",
                        "text": "Title"
                    }
                },
                {
                    "type": "input",
                    "block_id": "description_block",
                    "element": {
                        "type": "plain_text_input",
                        "action_id": "description_input",
                        "multiline": True,
                        "placeholder": {
                            "type": "plain_text",
                            "text": "Detailed description..."
                        }
                    },
                    "label": {
                        "type": "plain_text",
                        "text": "Description"
                    }
                },
                {
                    "type": "input",
                    "block_id": "priority_block",
                    "element": {
                        "type": "static_select",
                        "action_id": "priority_select",
                        "placeholder": {
                            "type": "plain_text",
                            "text": "Select priority"
                        },
                        "options": [
                            {
                                "text": {"type": "plain_text", "text": "Low"},
                                "value": "low"
                            },
                            {
                                "text": {"type": "plain_text", "text": "Medium"},
                                "value": "medium"
                            },
                            {
                                "text": {"type": "plain_text", "text": "High"},
                                "value": "high"
                            },
                            {
                                "text": {"type": "plain_text", "text": "Critical"},
                                "value": "critical"
                            }
                        ]
                    },
                    "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": "Select assignee"
                        }
                    },
                    "label": {
                        "type": "plain_text",
                        "text": "Assignee"
                    },
                    "optional": True
                },
                {
                    "type": "input",
                    "block_id": "due_date_block",
                    "element": {
                        "type": "datepicker",
                        "action_id": "due_date_picker",
                        "placeholder": {
                            "type": "plain_text",
                            "text": "Select a date"
                        }
                    },
                    "label": {
                        "type": "plain_text",
                        "text": "Due Date"
                    },
                    "optional": True
                }
            ]
        }
    )

@app.view("create_ticket_modal")
def handle_ticket_submission(ack, body, client, view, logger):
    """Handle ticket modal submission"""

    # Extract values
    values = view['state']['values']
    title = values['title_block']['title_input']['value']
    description = values['description_block']['description_input']['value']
    priority = values['priority_block']['priority_select']['selected_option']['value']
    assignee = values['assignee_block']['assignee_select'].get('selected_user')
    due_date = values['due_date_block']['due_date_picker'].get('selected_date')

    user = body['user']['id']

    # Validate input
    errors = {}
    if len(title) < 5:
        errors['title_block'] = "Title must be at least 5 characters"
    if len(description) < 10:
        errors['description_block'] = "Description must be at least 10 characters"

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

    ack()

    # Create ticket (in real app, save to database/API)
    ticket_id = f"TICKET-{hash(title) % 10000:04d}"

    # Notify in channel
    blocks = [
        {
            "type": "header",
            "text": {
                "type": "plain_text",
                "text": f":ticket: New Ticket Created",
                "emoji": True
            }
        },
        {
            "type": "section",
            "fields": [
                {"type": "mrkdwn", "text": f"*ID:*\n`{ticket_id}`"},
                {"type": "mrkdwn", "text": f"*Priority:*\n{priority.title()}"},
                {"type": "mrkdwn", "text": f"*Created by:*\n<@{user}>"},
                {"type": "mrkdwn", "text": f"*Assignee:*\n{'<@' + assignee + '>' if assignee else 'Unassigned'}"}
            ]
        },
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Title:*\n{title}\n\n*Description:*\n{description}"
            }
        }
    ]

    if due_date:
        blocks.append({
            "type": "context",
            "elements": [
                {"type": "mrkdwn", "text": f":calendar: Due: {due_date}"}
            ]
        })

    # Post to tickets channel
    client.chat_postMessage(
        channel="#tickets",
        blocks=blocks,
        text=f"New ticket: {title}"
    )

    # DM creator confirmation
    client.chat_postMessage(
        channel=user,
        text=f":white_check_mark: Your ticket `{ticket_id}` has been created!"
    )

    logger.info(f"Ticket {ticket_id} created by {user}")

# Multi-step modal workflow
@app.command("/onboard")
def start_onboarding(ack, body, client):
    """Start multi-step onboarding workflow"""
    ack()

    client.views_open(
        trigger_id=body['trigger_id'],
        view={
            "type": "modal",
            "callback_id": "onboard_step_1",
            "title": {"type": "plain_text", "text": "Onboarding (1/3)"},
            "submit": {"type": "plain_text", "text": "Next"},
            "close": {"type": "plain_text", "text": "Cancel"},
            "blocks": [
                {
                    "type": "section",
                    "text": {
                        "type": "mrkdwn",
                        "text": "Welcome! Let's set up your profile."
                    }
                },
                {
                    "type": "input",
                    "block_id": "name_block",
                    "element": {
                        "type": "plain_text_input",
                        "action_id": "full_name"
                    },
                    "label": {"type": "plain_text", "text": "Full Name"}
                },
                {
                    "type": "input",
                    "block_id": "role_block",
                    "element": {
                        "type": "plain_text_input",
                        "action_id": "role"
                    },
                    "label": {"type": "plain_text", "text": "Role/Title"}
                }
            ]
        }
    )

@app.view("onboard_step_1")
def handle_step_1(ack, body, client, view):
    """Handle step 1 and show step 2"""

    values = view['state']['values']
    name = values['name_block']['full_name']['value']
    role = values['role_block']['role']['value']

    # Update to step 2
    ack(response_action="update", view={
        "type": "modal",
        "callback_id": "onboard_step_2",
        "title": {"type": "plain_text", "text": "Onboarding (2/3)"},
        "submit": {"type": "plain_text", "text": "Next"},
        "close": {"type": "plain_text", "text": "Cancel"},
        "private_metadata": json.dumps({"name": name, "role": role}),
        "blocks": [
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"Great, *{name}*! Now select your team."
                }
            },
            {
                "type": "input",
                "block_id": "team_block",
                "element": {
                    "type": "static_select",
                    "action_id": "team_select",
                    "options": [
                        {"text": {"type": "plain_text", "text": "Engineering"}, "value": "engineering"},
                        {"text": {"type": "plain_text", "text": "Design"}, "value": "design"},
                        {"text": {"type": "plain_text", "text": "Product"}, "value": "product"},
                        {"text": {"type": "plain_text", "text": "Operations"}, "value": "operations"}
                    ]
                },
                "label": {"type": "plain_text", "text": "Team"}
            }
        ]
    })

@app.view("onboard_step_2")
def handle_step_2(ack, body, client, view):
    """Handle step 2 and show step 3 (final)"""

    values = view['state']['values']
    previous = json.loads(view['private_metadata'])
    team = values['team_block']['team_select']['selected_option']['value']

    previous['team'] = team

    ack(response_action="update", view={
        "type": "modal",
        "callback_id": "onboard_step_3",
        "title": {"type": "plain_text", "text": "Onboarding (3/3)"},
        "submit": {"type": "plain_text", "text": "Complete"},
        "close": {"type": "plain_text", "text": "Cancel"},
        "private_metadata": json.dumps(previous),
        "blocks": [
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": "Almost done! Any additional info?"
                }
            },
            {
                "type": "input",
                "block_id": "bio_block",
                "element": {
                    "type": "plain_text_input",
                    "action_id": "bio",
                    "multiline": True
                },
                "label": {"type": "plain_text", "text": "Short Bio"},
                "optional": True
            }
        ]
    })

@app.view("onboard_step_3")
def handle_final_step(ack, body, client, view, logger):
    """Complete onboarding"""
    ack()

    values = view['state']['values']
    previous = json.loads(view['private_metadata'])
    bio = values['bio_block']['bio'].get('value', 'No bio provided')

    user = body['user']['id']

    # Complete onboarding
    profile = {
        **previous,
        "bio": bio,
        "user_id": user
    }

    # Announce new team member
    client.chat_postMessage(
        channel="#general",
        blocks=[
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f":wave: Welcome <@{user}> to the team!\n\n*Role:* {profile['role']}\n*Team:* {profile['team'].title()}\n*Bio:* {bio}"
                }
            }
        ],
        text=f"Welcome {profile['name']} to the team!"
    )

    logger.info(f"Onboarding completed for {user}: {profile}")

5. Slash Commands

# commands.py
# ABOUTME: Slash command implementations
# ABOUTME: Various utility commands for team workflows

from slack_bolt import App
from datetime import datetime, timedelta
import random
import os

app = App(token=os.environ.get("SLACK_BOT_TOKEN"))

@app.command("/standup")
def handle_standup(ack, body, client, command):
    """Start a standup thread"""
    ack()

    channel = command['channel_id']
    user = command['user_id']

    # Create standup thread
    result = client.chat_postMessage(
        channel=channel,
        blocks=[
            {
                "type": "header",
                "text": {
                    "type": "plain_text",
                    "text": f":sunrise: Daily Standup - {datetime.now().strftime('%A, %B %d')}",
                    "emoji": True
                }
            },
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": "Please share your updates in this thread:\n\n1. :white_check_mark: What did you accomplish yesterday?\n2. :calendar: What are you working on today?\n3. :construction: Any blockers?"
                }
            },
            {
                "type": "divider"
            },
            {
                "type": "context",
                "elements": [
                    {"type": "mrkdwn", "text": f"Started by <@{user}>"}
                ]
            }
        ],
        text="Daily Standup"
    )

    # Pin the standup
    client.pins_add(channel=channel, timestamp=result['ts'])

@app.command("/poll")
def handle_poll(ack, body, client, command):
    """Create a quick poll: /poll "Question" "Option 1" "Option 2" ..."""
    ack()

    text = command.get('text', '')

    # Parse quoted arguments
    import re
    parts = re.findall(r'"([^"]+)"', text)

    if len(parts) < 3:
        client.chat_postEphemeral(
            channel=command['channel_id'],
            user=command['user_id'],
            text='Usage: /poll "Question" "Option 1" "Option 2" "Option 3"'
        )
        return

    question = parts[0]
    options = parts[1:]

    # Create poll blocks
    option_blocks = []
    emojis = [':one:', ':two:', ':three:', ':four:', ':five:', ':six:', ':seven:', ':eight:', ':nine:']

    for i, option in enumerate(options[:9]):
        option_blocks.append({
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"{emojis[i]} {option}"
            }
        })

    blocks = [
        {
            "type": "header",
            "text": {"type": "plain_text", "text": ":bar_chart: Poll", "emoji": True}
        },
        {
            "type": "section",
            "text": {"type": "mrkdwn", "text": f"*{question}*"}
        },
        {"type": "divider"},
        *option_blocks,
        {"type": "divider"},
        {
            "type": "context",
            "elements": [
                {"type": "mrkdwn", "text": f"Poll by <@{command['user_id']}> | React to vote!"}
            ]
        }
    ]

    result = client.chat_postMessage(
        channel=command['channel_id'],
        blocks=blocks,
        text=f"Poll: {question}"
    )

    # Add reaction options
    for i in range(len(options[:9])):
        emoji_names = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
        client.reactions_add(
            channel=command['channel_id'],
            timestamp=result['ts'],
            name=emoji_names[i]
        )

@app.command("/remind-team")
def handle_team_reminder(ack, body, client, command):
    """Set a team reminder: /remind-team 15m Check deployment status"""
    ack()

    text = command.get('text', '').strip()
    parts = text.split(' ', 1)

    if len(parts) < 2:
        client.chat_postEphemeral(
            channel=command['channel_id'],
            user=command['user_id'],
            text='Usage: /remind-team 15m Your reminder message'
        )
        return

    time_str = parts[0]
    message = parts[1]

    # Parse time
    time_map = {'s': 1, 'm': 60, 'h': 3600}
    unit = time_str[-1]

    if unit not in time_map:
        client.chat_postEphemeral(
            channel=command['channel_id'],
            user=command['user_id'],
            text='Time format: 15s, 15m, or 2h'
        )
        return

    try:
        amount = int(time_str[:-1])
        seconds = amount * time_map[unit]
    except ValueError:
        client.chat_postEphemeral(
            channel=command['channel_id'],
            user=command['user_id'],
            text='Invalid time format'
        )
        return

    # Schedule message
    post_at = int(datetime.now().timestamp()) + seconds

    client.chat_scheduleMessage(
        channel=command['channel_id'],
        post_at=post_at,
        text=f":bell: *Reminder:* {message}\n\n_Set by <@{command['user_id']}>_"
    )

    client.chat_postEphemeral(
        channel=command['channel_id'],
        user=command['user_id'],
        text=f"Reminder scheduled for {time_str} from now!"
    )

@app.command("/random-pick")
def handle_random_pick(ack, body, client, command):
    """Randomly pick from options: /random-pick option1 option2 option3"""
    ack()

    text = command.get('text', '').strip()

    if not text:
        client.chat_postEphemeral(
            channel=command['channel_id'],
            user=command['user_id'],
            text='Usage: /random-pick option1 option2 option3'
        )
        return

    options = text.split()
    picked = random.choice(options)

    client.chat_postMessage(
        channel=command['channel_id'],
        blocks=[
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f":game_die: <@{command['user_id']}> asked me to pick randomly from: {', '.join(options)}"
                }
            },
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f":point_right: *{picked}*"
                }
            }
        ],
        text=f"Random pick: {picked}"
    )

6. Webhooks and Incoming Messages

# webhooks.py
# ABOUTME: Incoming webhook integration for external services
# ABOUTME: CI/CD notifications, alerts, and external triggers

import requests
import json
from typing import Optional, List, Dict
import hmac
import hashlib
import time

class SlackWebhook:
    """Incoming webhook client for Slack"""

    def __init__(self, webhook_url: str):
        self.webhook_url = webhook_url

    def send(
        self,
        text: str,
        blocks: Optional[List[Dict]] = None,
        attachments: Optional[List[Dict]] = None,
        thread_ts: Optional[str] = None,
        unfurl_links: bool = True,
        unfurl_media: bool = True
    ) -> dict:
        """Send a message via webhook"""

        payload = {
            "text": text,
            "unfurl_links": unfurl_links,
            "unfurl_media": unfurl_media
        }

        if blocks:
            payload["blocks"] = blocks
        if attachments:
            payload["attachments"] = attachments
        if thread_ts:
            payload["thread_ts"] = thread_ts

        response = requests.post(
            self.webhook_url,
            json=payload,
            headers={"Content-Type": "application/json"}
        )

        response.raise_for_status()
        return {"ok": True, "status": response.status_code}

    def send_deployment_notification(
        self,
        app_name: str,
        environment: str,
        version: str,
        status: str,
        commit_sha: str,
        author: str,
        url: Optional[str] = None
    ):
        """Send a deployment notification"""

        color_map = {
            "success": "#36a64f",
            "failure": "#ff0000",
            "started": "#ffcc00",
            "pending": "#808080"
        }

        status_emoji = {
            "success": ":white_check_mark:",
            "failure": ":x:",
            "started": ":rocket:",
            "pending": ":hourglass:"
        }

        blocks = [
            {
                "type": "header",
                "text": {
                    "type": "plain_text",
                    "text": f"{status_emoji.get(status, ':grey_question:')} Deployment {status.title()}: {app_name}",
                    "emoji": True
                }
            },
            {
                "type": "section",
                "fields": [
                    {"type": "mrkdwn", "text": f"*Environment:*\n{environment}"},
                    {"type": "mrkdwn", "text": f"*Version:*\n{version}"},
                    {"type": "mrkdwn", "text": f"*Commit:*\n`{commit_sha[:8]}`"},
                    {"type": "mrkdwn", "text": f"*Author:*\n{author}"}
                ]
            }
        ]

        if url:
            blocks.append({
                "type": "actions",
                "elements": [
                    {
                        "type": "button",
                        "text": {"type": "plain_text", "text": "View Deployment"},
                        "url": url,
                        "style": "primary" if status == "success" else None
                    }
                ]
            })

        attachments = [
            {
                "color": color_map.get(status, "#808080"),
                "blocks": blocks
            }
        ]

        return self.send(
            text=f"Deployment {status}: {app_name} to {environment}",
            attachments=attachments
        )

    def send_alert(
        self,
        title: str,
        message: str,
        severity: str = "warning",
        source: str = "System",
        details: Optional[Dict] = None
    ):
        """Send an alert notification"""

        severity_config = {
            "critical": {"emoji": ":rotating_light:", "color": "#ff0000"},
            "error": {"emoji": ":x:", "color": "#ff4444"},
            "warning": {"emoji": ":warning:", "color": "#ffcc00"},
            "info": {"emoji": ":information_source:", "color": "#0088ff"}
        }

        config = severity_config.get(severity, severity_config["info"])

        blocks = [
            {
                "type": "header",
                "text": {
                    "type": "plain_text",
                    "text": f"{config['emoji']} {title}",
                    "emoji": True
                }
            },
            {
                "type": "section",
                "text": {"type": "mrkdwn", "text": message}
            },
            {
                "type": "context",
                "elements": [
                    {"type": "mrkdwn", "text": f"*Source:* {source} | *Severity:* {severity.upper()}"}
                ]
            }
        ]

        if details:
            detail_text = "\n".join(f"*{k}:* {v}" for k, v in details.items())
            blocks.insert(2, {
                "type": "section",
                "text": {"type": "mrkdwn", "text": detail_text}
            })

        return self.send(
            text=f"[{severity.upper()}] {title}",
            attachments=[{"color": config["color"], "blocks": blocks}]
        )

# Webhook signature verification
def verify_slack_signature(
    signing_secret: str,
    request_body: str,
    timestamp: str,
    signature: str
) -> bool:
    """Verify Slack request signature"""

    # Check timestamp to prevent replay attacks
    if abs(time.time() - int(timestamp)) > 60 * 5:
        return False

    sig_basestring = f"v0:{timestamp}:{request_body}"

    computed_signature = 'v0=' + hmac.new(
        signing_secret.encode(),
        sig_basestring.encode(),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(computed_signature, signature)

# Usage example
if __name__ == "__main__":
    webhook = SlackWebhook("https://hooks.slack.com/services/T00/B00/XXX")

    # Send deployment notification
    webhook.send_deployment_notification(
        app_name="my-service",
        environment="production",
        version="v1.2.3",
        status="success",
        commit_sha="abc123def",
        author="developer@example.com",
        url="https://deployments.example.com/123"
    )

    # Send alert
    webhook.send_alert(
        title="High CPU Usage",
        message="Server cpu-usage has exceeded 90% for the last 5 minutes.",
        severity="warning",
        source="Monitoring",
        details={
            "Server": "prod-web-01",
            "Current Usage": "92%",
            "Threshold": "90%"
        }
    )

Integration Examples

GitHub Actions Integration

# .github/workflows/slack-notify.yml
name: Slack Notifications

on:
  push:
    branches: [main]
  pull_request:
    types: [opened, closed, merged]
  workflow_run:
    workflows: ["CI"]
    types: [completed]

jobs:
  notify-deployment:
    runs-on: ubuntu-latest
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    steps:
      - name: Notify Slack
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plain_text",
                    "text": ":rocket: Deployment Started"
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {"type": "mrkdwn", "text": "*Repository:*\n${{ github.repository }}"},
                    {"type": "mrkdwn", "text": "*Branch:*\n${{ github.ref_name }}"},
                    {"type": "mrkdwn", "text": "*Commit:*\n`${{ github.sha }}`"},
                    {"type": "mrkdwn", "text": "*Author:*\n${{ github.actor }}"}
                  ]
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": {"type": "plain_text", "text": "View Commit"},
                      "url": "${{ github.event.head_commit.url }}"
                    }
                  ]
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK

FastAPI Integration

# api_integration.py
# ABOUTME: FastAPI integration for Slack event handling
# ABOUTME: Webhook endpoint for Slack Events API

from fastapi import FastAPI, Request, HTTPException
from slack_bolt import App
from slack_bolt.adapter.fastapi import SlackRequestHandler
import os

# Initialize Slack app
slack_app = App(
    token=os.environ.get("SLACK_BOT_TOKEN"),
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET")
)

# Register event handlers
@slack_app.event("message")
def handle_message(event, say):
    if "hello" in event.get("text", "").lower():
        say(f"Hi <@{event['user']}>!")

@slack_app.command("/api-status")
def handle_status(ack, respond):
    ack()
    respond("API is healthy!")

# FastAPI setup
app = FastAPI(title="Slack Bot API")
handler = SlackRequestHandler(slack_app)

@app.post("/slack/events")
async def slack_events(request: Request):
    """Handle Slack events"""
    return await handler.handle(request)

@app.post("/slack/interactions")
async def slack_interactions(request: Request):
    """Handle Slack interactive components"""
    return await handler.handle(request)

@app.get("/health")
async def health():
    """Health check endpoint"""
    return {"status": "healthy"}

Best Practices

1. Rate Limiting

# Rate limit handling
import time
from functools import wraps

def rate_limit_handler(max_retries=3, base_delay=1):
    """Decorator for handling Slack rate limits"""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if "rate_limited" in str(e):
                        delay = base_delay * (2 ** attempt)
                        time.sleep(delay)
                    else:
                        raise
            raise Exception("Max retries exceeded")
        return wrapper
    return decorator

@rate_limit_handler(max_retries=3)
def send_message(client, channel, text):
    return client.chat_postMessage(channel=channel, text=text)

2. Error Handling

# Comprehensive error handling
from slack_sdk.errors import SlackApiError

def safe_send_message(client, channel, text, blocks=None):
    """Send message with error handling"""
    try:
        result = client.chat_postMessage(
            channel=channel,
            text=text,
            blocks=blocks
        )
        return result
    except SlackApiError as e:
        error_code = e.response.get("error", "unknown_error")

        if error_code == "channel_not_found":
            # Handle missing channel
            raise ValueError(f"Channel {channel} not found")
        elif error_code == "not_in_channel":
            # Try to join channel first
            client.conversations_join(channel=channel)
            return client.chat_postMessage(channel=channel, text=text, blocks=blocks)
        elif error_code == "ratelimited":
            # Wait and retry
            retry_after = int(e.response.headers.get("Retry-After", 1))
            time.sleep(retry_after)
            return safe_send_message(client, channel, text, blocks)
        else:
            raise

3. Message Formatting

# Safe message formatting
def escape_text(text: str) -> str:
    """Escape special characters for Slack"""
    text = text.replace("&", "&amp;")
    text = text.replace("<", "&lt;")
    text = text.replace(">", "&gt;")
    return text

def format_user_mention(user_id: str) -> str:
    """Format user mention"""
    return f"<@{user_id}>"

def format_channel_link(channel_id: str) -> str:
    """Format channel link"""
    return f"<#{channel_id}>"

def format_url(url: str, text: str = None) -> str:
    """Format URL with optional text"""
    if text:
        return f"<{url}|{escape_text(text)}>"
    return f"<{url}>"

def format_code_block(code: str, language: str = "") -> str:
    """Format code block"""
    return f"```{language}\n{code}\n```"

4. Token Security

# Secure token management
import os
from functools import lru_cache

@lru_cache()
def get_slack_client():
    """Get cached Slack client with secure token"""
    from slack_sdk import WebClient

    token = os.environ.get("SLACK_BOT_TOKEN")
    if not token:
        raise ValueError("SLACK_BOT_TOKEN not set")

    if not token.startswith("xoxb-"):
        raise ValueError("Invalid bot token format")

    return WebClient(token=token)

# Never log tokens
import logging
class TokenFilter(logging.Filter):
    def filter(self, record):
        if hasattr(record, 'msg'):
            record.msg = str(record.msg).replace(
                os.environ.get("SLACK_BOT_TOKEN", ""), "[REDACTED]"
            )
        return True

Troubleshooting

Common Issues

Issue: Bot not responding to messages

# Verify bot has correct scopes
# Check Event Subscriptions are enabled
# Ensure Request URL is verified

# Debug with logging
import logging
logging.basicConfig(level=logging.DEBUG)

@app.event("message")
def debug_messages(body, logger):
    logger.info(f"Received message event: {body}")

Issue: Interactive components not working

# Ensure Interactive Components URL is set
# Check action_id matches handler

@app.action("button_click")  # Must match action_id in block
def handle_click(ack, body, logger):
    ack()
    logger.info(f"Button clicked: {body}")

Issue: Socket Mode connection drops

# Increase connection timeout
from slack_bolt.adapter.socket_mode import SocketModeHandler

handler = SocketModeHandler(
    app,
    app_token,
    ping_interval=30  # Send ping every 30 seconds
)

Issue: Message not appearing in channel

# Check channel ID format
# Verify bot is in channel

def ensure_in_channel(client, channel):
    try:
        client.conversations_info(channel=channel)
    except:
        client.conversations_join(channel=channel)

Debug Commands

# Test webhook
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"text": "Test message"}' \
  $SLACK_WEBHOOK_URL

# Test bot token
curl -X POST \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  https://slack.com/api/auth.test

# List channels
curl -X GET \
  -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  "https://slack.com/api/conversations.list?limit=10"

Version History

Version Date Changes
1.0.0 2026-01-17 Initial release with comprehensive Slack API patterns

Resources


This skill provides production-ready patterns for Slack bot development, enabling powerful team automation and interactive workflows.

Weekly Installs
13
First Seen
Jan 24, 2026
Installed on
claude-code12
opencode11
cursor11
codex10
gemini-cli10
antigravity9