Skip to main content
This guide shows how to create a Slack bot using Timbal that can:
  • Respond to messages in real-time via webhooks
  • Process user requests intelligently with AI agents
  • Handle threaded conversations

Setup Requirements

Install the Slack SDK:
pip install slack-sdk
Or using uv (recommended):
uv add slack-sdk
For all available Slack API methods and capabilities, see the Slack API Methods Reference.
1

Create a Slack App

  1. Go to Slack API and click Create New App
  2. Choose From scratch
  3. Enter your app name and select your workspace
2

Configure Bot Token Scopes

  1. Go to OAuth & Permissions
  2. Add the following Bot Token Scopes based on Timbal handlers you’ll use:
# Message Operations
chat:write            # Send messages
chat:write.public     # Send messages to channels bot isn't in
im:write             # Send direct messages

# Channel & Conversation Operations
channels:read        # View basic channel information
channels:history     # Get messages from channels
groups:read          # View basic private channel information
im:read              # View basic direct message information
im:history           # Get direct message history

# App Mentions & Reactions
app_mentions:read    # Receive app mention events
reactions:read       # Read message reactions

# File Operations
files:read           # Download files from Slack
files:write          # Upload files to Slack
After adding or modifying scopes, you’ll need to reinstall the app to your workspace for the changes to take effect.
3

Install App to Workspace

  1. In OAuth & Permissions, click Install to Workspace
  2. Copy the Bot User OAuth Token (starts with xoxb-)
  3. Set as environment variable: SLACK_BOT_TOKEN
4

Enable Home Tab (for Direct Messages)

  1. Go to App Home in your Slack app settings
  2. Under Show Tabs, enable Home Tab
  3. Check Allow users to send Slash commands and messages from the messages tab
  4. This enables users to send direct messages to your bot
5

Get Bot User ID

  1. In your Slack workspace, start a direct message with your bot
  2. Click on the bot’s name at the top of the chat
  3. In the profile panel, find the Member ID (format: U12345678)
  4. Set this as environment variable: SLACK_BOT_USER_ID=U12345678
The Bot User ID is required to prevent infinite loops by filtering out the bot’s own messages.

Implementation

Setting up the Slack Bot Configuration

First, set up the Slack configuration with your bot’s user ID and authentication token. Create a .env file in your project root:
.env
SLACK_BOT_USER_ID=U12345678  # Your actual bot user ID
SLACK_BOT_TOKEN=xoxb-your-bot-token-here  # Your bot token
Then configure your Python code:
import os

from dotenv import load_dotenv
from slack_sdk import WebClient

# Load environment variables from .env
load_dotenv()

SLACK_BOT_USER_ID = os.getenv("SLACK_BOT_USER_ID")

client = WebClient(token=os.getenv("SLACK_BOT_TOKEN"))

Creating the Agent

Create the Timbal agent with pre and post hooks for Slack integration:
from timbal import Agent

agent = Agent(
    name="SlackAgent",
    system_prompt="You are a tech support AI assistant. Help users with their technical questions clearly and concisely.",
    model="openai/gpt-4.1-mini",
    pre_hook=pre_hook,
    post_hook=post_hook,
)

Pre-hook: Processing Incoming Messages

The pre-hook handles incoming Slack webhook events and extracts relevant information.
Parameter Name Flexibility: The _webhook parameter name is completely custom. You can use any name you prefer:
  • await agent(slack_data=body).collect()
  • await agent(webhook_payload=body).collect()
  • await agent(event_data=body).collect()
Just ensure the same name is used consistently in both the agent call and the pre_hook check.
from timbal.state import get_run_context
from timbal.errors import bail

async def pre_hook():
    """Process incoming Slack webhook events before agent execution."""
    span = get_run_context().current_span()

    # Check if this is a Slack webhook event
    # Note: "_webhook" matches the parameter name used when calling agent(_webhook=body)
    if "_webhook" not in span.input:
        return  # Not triggered by Slack

    slack_event = span.input["_webhook"]["event"]

    # Ignore bot's own messages to prevent infinite loops
    if slack_event.get("user") == SLACK_BOT_USER_ID:
        raise bail()

    # Extract and validate message text
    text = slack_event.get("text", "")
    if not text:
        raise bail()

    # Store Slack context for response
    span.slack_channel = slack_event["channel"]
    span.slack_thread_ts = slack_event.get("thread_ts")  # Reply in same thread
    span.input["prompt"] = text

Post-hook: Sending Responses

The post-hook sends the agent’s response back to Slack:
from timbal.state import get_run_context

def post_hook():
    """Send agent response back to Slack channel."""
    span = get_run_context().current_span()
    
    # Only process Slack webhook events
    if "_webhook" not in span.input:
        return

    # Extract response context
    slack_channel = span.slack_channel
    slack_thread_ts = span.slack_thread_ts
    reply = span.output.content[0].text

    # Send response if not empty
    if reply.strip():
        client.chat_postMessage(
            channel=slack_channel,
            text=reply,
            thread_ts=slack_thread_ts,
        )

Complete Example

Here’s the full implementation ready to use:
agent.py
import os

from dotenv import load_dotenv
from slack_sdk import WebClient
from timbal import Agent
from timbal.errors import bail
from timbal.state import get_run_context

# Load environment variables from .env
load_dotenv()

SLACK_BOT_USER_ID = os.getenv("SLACK_BOT_USER_ID")

client = WebClient(token=os.getenv("SLACK_BOT_TOKEN"))

async def pre_hook():
    """Process incoming Slack webhook events before agent execution."""
    span = get_run_context().current_span()

    # Only process Slack webhook events
    # Note: "_webhook" must match the parameter name used when calling agent(_webhook=body)
    if "_webhook" not in span.input:
        return

    slack_event = span.input["_webhook"]["event"]

    # Prevent infinite loops by ignoring bot's own messages
    if slack_event.get("user") == SLACK_BOT_USER_ID:
        raise bail()

    # Extract message text
    text = slack_event.get("text", "")
    if not text:
        raise bail()

    # Store context for response
    span.slack_channel = slack_event["channel"]
    span.slack_thread_ts = slack_event.get("thread_ts")
    span.input["prompt"] = text

def post_hook():
    """Send agent response back to Slack."""
    span = get_run_context().current_span()
    
    if "_webhook" not in span.input:
        return

    # Get response and send to Slack
    reply = span.output.content[0].text
    slack_channel = span.slack_channel
    slack_thread_ts = span.slack_thread_ts
    
    if reply.strip():
        client.chat_postMessage(
            channel=slack_channel,
            text=reply,
            thread_ts=slack_thread_ts,
        )

# Create the Slack agent
agent = Agent(
    name="SlackAgent",
    system_prompt="You are a tech support AI assistant. Help users with their technical questions clearly and concisely.",
    model="openai/gpt-4.1-mini",
    pre_hook=pre_hook,
    post_hook=post_hook,
)

Running the Agent

Timbal provides built-in ngrok integration for easy local development. To use this feature:
1

Set up ngrok account and authtoken

  1. Create an account at ngrok.com
  2. Install ngrok on your system and the Python package:
pip install pyngrok
Or using uv (recommended):
uv add pyngrok
  1. Configure your authtoken:
ngrok config add-authtoken YOUR_AUTHTOKEN
2

Create FastAPI Webhook Handler

Create a simple FastAPI server to handle Slack’s URL verification and webhook events:
server.py
import uvicorn
from dotenv import load_dotenv
from fastapi import FastAPI, Request
from pyngrok import ngrok

# Import our Slack agent to handle webhook events
from agent import agent

load_dotenv()

app = FastAPI()

@app.post("/")
async def slack_events(request: Request):
    body = await request.json()
    
    if body.get("type") == "url_verification":
        return {"challenge": body.get("challenge")}
    
    # Process actual Slack events here
    if body.get("type") == "event_callback":
        await agent(_webhook=body).collect()  # _webhook is a custom parameter name you define
        
    return {"status": "ok"}

if __name__ == "__main__":
    port = 8000
    public_url = ngrok.connect(port, "http")
    print(f"Public URL: {public_url}")
    
    uvicorn.run(app, host="0.0.0.0", port=port)
3

Run the Server and Get ngrok URL

  1. Run the script to start your Slack bot:
python server.py
Or using uv (recommended):
uv run server.py
  1. Copy the ngrok public URL displayed in the terminal (e.g., https://abc123.ngrok-free.app)
4

Enable Event Subscriptions

  1. Go to Event Subscriptions and toggle Enable Events
  2. Set Request URL to our webhook endpoint (e.g., https://abc123.ngrok-free.app/)
  3. Subscribe to Bot Events - these determine what events Slack will send to your bot:
message.channels  # Bot receives messages in public channels it's added to
message.groups    # Bot receives messages in private channels it's added to
message.im        # Bot receives direct messages sent to it
app_mention       # Bot receives messages that mention it (@botname)
reaction_added    # Bot receives events when reactions are added to messages
Your Slack bot is now ready! 🚀 Start a direct message with your bot or mention it in a channel to begin chatting with your AI assistant.

Key Features

  • Real-time Responses: Instantly processes Slack messages via webhooks
  • Thread Support: Maintains conversation context in Slack threads
  • Loop Prevention: Automatically ignores bot’s own messages
  • Flexible AI: Uses any LLM model supported by Timbal
  • Easy Integration: Minimal setup with existing Slack handlers

Troubleshooting

  • Check webhook URL is publicly accessible and returns 200 status
  • Ensure bot has correct permissions in Slack workspace
  • Confirm SLACK_BOT_USER_ID matches your bot’s actual user ID
  • Test webhook endpoint manually with curl or Postman
  • Double-check SLACK_BOT_USER_ID is correct (found in Slack app settings)
  • Ensure pre-hook properly filters bot messages with bail()
  • Verify webhook events aren’t being duplicated
  • Check that bot doesn’t respond to its own message events
  • Check that OpenAI API key is valid and has sufficient credits
  • Verify agent model name is correct (e.g., openai/gpt-4o-mini)
  • Ensure pre-hook is setting span.input["prompt"] correctly
  • Add logging to debug webhook payload structure
  • Ensure all required packages are installed (slack-sdk fastapi uvicorn pyngrok python-dotenv)
  • Verify ngrok is installed and configured with authtoken
  • Make sure port 8000 is not already in use by another process
  • Ensure Slack Request URL matches your POST endpoint route with ”/” (e.g., https://abc123.ngrok-free.app/)