Agent DailyAgent Daily
tutorialintermediate

Build a Slack data analyst bot with Claude Managed Agents Apr 2026 • Agent Patterns Integrations Mention the bot with a CSV to get an analysis report in-thread, with multi-turn follow-ups on the same session.

cookbook
View original on cookbook

This cookbook demonstrates building a Slack data analyst bot using Claude Managed Agents and Bolt for Python. Users mention the bot with a CSV file to receive narrative analysis reports in-thread, with support for multi-turn follow-ups within the same session. The implementation handles file uploads, streams agent progress updates, and manages session persistence across Slack threads.

Key Points

  • Create a Slack app from manifest with Socket Mode enabled to avoid needing a public URL
  • Obtain two Slack tokens: Bot User OAuth Token (xoxb-) and App-Level Token with connections:write scope (xapp-)
  • Use threading to handle slow operations (file upload, session creation) without blocking Slack's 3-second acknowledgment deadline
  • Upload CSV files from Slack to Anthropic Files API and mount them in the session for agent access
  • Store thread_ts to session_id mapping to maintain conversation context across follow-up messages in the same thread
  • Stream agent progress events from Anthropic API and relay them as thread updates to provide real-time feedback
  • Filter downloadable files to post only agent-generated outputs (reports, charts) back to Slack, excluding user inputs
  • Persist session metadata (Slack channel, thread timestamp) for tracking and debugging agent runs
  • Handle session lifecycle: create on mention, stream progress, post final summary, and manage follow-up replies
  • Convert Markdown to Slack's mrkdwn format for proper formatting of agent responses in Slack messages

Found this useful? Add it to a playbook for a step-by-step implementation guide.

Workflow Diagram

Start Process
Step A
Step B
Step C
Complete
Quality

Concepts

Artifacts (7)

slack_bot_setup.pypythonscript
import io
import os
import threading
from getpass import getpass
import requests
from anthropic import Anthropic
from dotenv import load_dotenv, set_key
from markdown_to_mrkdwn import SlackMarkdownConverter
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

load_dotenv(override=True)

# Prompt for Slack tokens on first run and save them to .env
for key in ("SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"):
    if not os.environ.get(key):
        os.environ[key] = getpass(f"{key}: ")
        set_key(".env", key, os.environ[key])

client = Anthropic()
app = App(token=os.environ["SLACK_BOT_TOKEN"])

for key in ("ANALYST_ENV_ID", "ANALYST_AGENT_ID", "ANALYST_AGENT_VERSION"):
    if not os.environ.get(key):
        raise RuntimeError(f"{key} not set. Run data_analyst_agent.ipynb first.")

ANALYST_AGENT = {
    "id": os.environ["ANALYST_AGENT_ID"],
    "version": int(os.environ["ANALYST_AGENT_VERSION"]),
}
ANALYST_ENV_ID = os.environ["ANALYST_ENV_ID"]

# thread_ts -> session_id mapping for follow-ups
thread_sessions: dict[str, str] = {}
mrkdwn = SlackMarkdownConverter()
slack_bot_mention_handler.pypythonscript
@app.event("app_mention")
def on_mention(event, say, ack):
    ack()
    channel = event["channel"]
    thread_ts = event.get("thread_ts") or event["ts"]
    # Mention text arrives as "<@BOTID> question"; drop the mention prefix
    question = event["text"].split(">", 1)[-1].strip()
    slack_file = (event.get("files") or [None])[0]
    
    say(text="On it. Analyzing now.", thread_ts=thread_ts)
    
    # Run slow work in background thread
    threading.Thread(
        target=start_analysis,
        args=(channel, thread_ts, question, slack_file)
    ).start()

def start_analysis(channel: str, thread_ts: str, question: str, slack_file: dict | None) -> None:
    try:
        resources = []
        if slack_file:
            # Download file from Slack
            resp = requests.get(
                slack_file["url_private"],
                headers={"Authorization": f"Bearer {app.client.token}"},
                timeout=30,
            )
            resp.raise_for_status()
            mime = slack_file.get("mimetype", "text/csv")
            
            # Upload to Anthropic Files API
            uploaded = client.beta.files.upload(
                file=(slack_file["name"], io.BytesIO(resp.content), mime)
            )
            mount = "/mnt/session/uploads/data.csv"
            resources.append({
                "type": "file",
                "file_id": uploaded.id,
                "mount_path": mount
            })
            question += f"\n\nThe data is mounted at {mount}."
        
        # Create session per Slack thread
        session = client.beta.sessions.create(
            environment_id=ANALYST_ENV_ID,
            agent={"type": "agent", **ANALYST_AGENT},
            resources=resources,
            title="".join(c for c in question if c.isprintable())[:80],
            metadata={
                "slack_channel": channel,
                "slack_thread_ts": thread_ts
            },
        )
        thread_sessions[thread_ts] = session.id
        
        # Send question to agent
        client.beta.sessions.events.send(
            session.id,
            events=[{
                "type": "user.message",
                "content": [{"type": "text", "text": question}]
            }],
        )
        relay_stream(session.id, channel, thread_ts)
    except Exception as e:
        app.client.chat_postMessage(
            channel=channel,
            thread_ts=thread_ts,
            text=f"Analysis failed: {type(e).__name__}: {e}"
        )
slack_bot_relay_stream.pypythonscript
def relay_stream(session_id: str, channel: str, thread_ts: str) -> None:
    summary = ""
    posted_progress = False
    
    for ev in client.beta.sessions.events.stream(session_id):
        t = ev.type
        if t == "agent.message":
            # Keep the latest text block; it becomes the final summary
            for b in ev.content:
                if b.type == "text" and b.text.strip():
                    summary = b.text
        elif t == "agent.tool_use" and not posted_progress:
            # Post one-time progress update when agent starts running commands
            app.client.chat_postMessage(
                channel=channel,
                thread_ts=thread_ts,
                text="Running analysis..."
            )
            posted_progress = True
        elif t == "session.status_idle":
            break
        elif t == "session.status_terminated":
            trace = f"https://platform.claude.com/sessions/{session_id}"
            app.client.chat_postMessage(
                channel=channel,
                thread_ts=thread_ts,
                text=f"Session terminated. [View trace]({trace})"
            )
            return
    
    # Post final summary
    if summary:
        app.client.chat_postMessage(
            channel=channel,
            thread_ts=thread_ts,
            text=mrkdwn.convert(summary)
        )
    
    # Upload agent-generated files
    for f in client.beta.files.list(scope_id=session_id):
        if f.downloadable:
            content = client.beta.files.download(f.id).content
            app.client.files_upload_v2(
                channel=channel,
                thread_ts=thread_ts,
                file=content,
                filename=f.filename
            )
slack_bot_followup_handler.pypythonscript
@app.event("message")
def on_message(event, say, ack):
    ack()
    
    # Only handle replies in threads where bot has a session
    thread_ts = event.get("thread_ts")
    if not thread_ts or thread_ts not in thread_sessions:
        return
    
    # Skip bot's own messages
    if event.get("bot_id"):
        return
    
    session_id = thread_sessions[thread_ts]
    channel = event["channel"]
    text = event.get("text", "")
    
    say(text="Processing follow-up...", thread_ts=thread_ts)
    
    # Send follow-up to same session
    try:
        client.beta.sessions.events.send(
            session_id,
            events=[{
                "type": "user.message",
                "content": [{"type": "text", "text": text}]
            }],
        )
        relay_stream(session_id, channel, thread_ts)
    except Exception as e:
        app.client.chat_postMessage(
            channel=channel,
            thread_ts=thread_ts,
            text=f"Follow-up failed: {type(e).__name__}: {e}"
        )
slack_bot_main.pypythonscript
# Main entry point - run the bot with Socket Mode
if __name__ == "__main__":
    handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
    handler.start()
requirements.txtconfig
anthropic>=0.91.0
python-dotenv
slack-bolt
requests
markdown-to-mrkdwn
.env.exampleconfig
SLACK_BOT_TOKEN=xoxb-your-token-here
SLACK_APP_TOKEN=xapp-your-token-here
ANALYST_ENV_ID=your-env-id
ANALYST_AGENT_ID=your-agent-id
ANALYST_AGENT_VERSION=1