Run a research analyst in Slack without building agent infrastructure. You @mention the bot with a question, and it searches and fetches sources inside an Anthropic-managed sandbox, then streams a sourced brief back into the thread.
Each Slack thread maps to a single persistent Claude Managed Agents session, so follow-ups retain the thread's prior research context. The bot is built with Chat SDK, the unified TypeScript SDK for building chat bots
Vercel Connect owns the Slack app and brokers its credentials at runtime, so there's no bot token or signing secret to manage. Upstash Redis preserves subscriptions, thread-to-session mappings, and webhook deduplication across deployments.
Deploy the template now, or read on for a deeper look at how it all works.
Claude Research Analyst for Slack
A Slack bot that turns @mentions into sourced research briefs, with one persistent Claude session per thread.
Copy link to headingThe stack
| Layer | Choice |
|---|---|
| Surface | Slack via Chat SDK and Vercel Connect |
| Agent | Claude Managed Agents with Claude Sonnet 5 |
| State | Upstash Redis |
| Runtime | Next.js 16 on Vercel |
Each layer handles a distinct job:
- Managed Agents: Runs the model loop, the sandbox, and the web tools.
- Chat SDK: Handles the Slack surface, including mentions, threads, typing indicators, and streamed posts.
- Vercel Connect: Manages the Slack app and its credentials.
- Redis: Keeps the state that has to survive redeploys.
That leaves your code as a thin bridge between them, about six source files.
Copy link to headingSetup and deployment
Copy link to headingWhat you need before deploying
You need three accounts to deploy and run the analyst:
- A Vercel account.
- A Slack workspace where you can install an app.
- An Anthropic API key with Managed Agents access.
For local development, you also need Node.js 24+, pnpm, and the Vercel CLI.
Copy link to headingDeploy to Vercel
The one-click flow is the fastest path.
It forks the template into a repository under your GitHub account, creates the Vercel project, and provisions the Slack and Redis pieces:
- A Slack connector, with its event trigger pointed at the bot’s webhook route (
/api/webhooks/slack) and the connector UID stored inSLACK_CONNECTOR. - An Upstash Redis store from the Vercel Marketplace.
The flow also prompts for the Claude Managed Agents credentials: ANTHROPIC_API_KEY, CLAUDE_AGENT_ID, and CLAUDE_ENVIRONMENT_ID. Get all three from the Claude Console and paste them in.
One thing to remember for later: when you want to customize the analyst, clone the forked repository under your GitHub account, not the vercel-labs template.
Copy link to headingSet up from a clone
If you'd rather provision everything manually, start from the template repository:
Link your Vercel project, create the Slack connector with the Vercel CLI, and then add Upstash Redis from the Vercel Marketplace:
Vercel Connect delivers Slack events to the attached project environment, so test the inbound Slack path against a deployment rather than a local dev server.
Next, create the Managed Agent.
The analyst is a persistent Anthropic resource, created once and reused by every deployment. Add ANTHROPIC_API_KEY to .env.local, then run:
This one-time command creates two resources and wires them up:
- The agent itself: name, description, model, system prompt, and tool policy.
- Cloud sandbox environment with network access, where the agent's web searches and fetches run.
The command writes CLAUDE_AGENT_ID and CLAUDE_ENVIRONMENT_ID to .env.local and, with the --vercel flag, adds both to the linked Vercel project's environment variables.
If the IDs already exist locally, rerunning copies them to Vercel instead of creating duplicates. Setup refuses to run twice otherwise, because agents and environments are persistent resources. Use pnpm cma:update for changes.
Finally, add your API key to the project and deploy:
Copy link to headingInvite the bot
After the production deployment finishes, invite the bot to the channels where it should answer, or message it directly. In a thread with one human participant, follow-ups don't need another @mention. Once multiple humans participate, the bot responds only when explicitly mentioned.
Copy link to headingHow the research analyst works
The bot runs one loop per message: apply the participant policy, resolve the thread's session, queue the turn, and stream the reply.
- @mention in Slack: A team member @mentions the bot in a channel or DMs it a research question. Chat SDK subscribes the bot to the thread and routes the message to the handler.
- Apply the participant policy: In a thread with one human, every message gets a response. When a second human joins, the bot answers only explicit mentions.
- Resolve the session: The thread's Redis state contains a Managed Agents session ID, which is validated server-side before use. If the session is gone, the bot creates a fresh one and says so.
- Queue the turn: Turns within a thread run one at a time, so rapid messages can't interleave their replies.
- Research in the sandbox: Claude Sonnet 5 plans, searches for, and fetches sources within the secure sandbox environment.
- Stream the brief: The bot subscribes to the session's event stream and posts the reply into Slack as the agent writes it, sources named inline.
Copy link to headingCode walkthrough
The runtime is six files under src/, plus a small CLI under scripts/cma/ for managing the persistent Anthropic resources.
Copy link to headingThe Slack surface
The whole chat surface is one file, src/lib/bot.ts:
connectSlackAdapter reads the connector UID from SLACK_CONNECTOR and lets Vercel Connect handle app credentials, token rotation, and webhook verification, so none of that lives in your code.
Redis state makes subscriptions, deduplication, and per-thread data durable across serverless instances and redeploys, which is why the template requires it rather than in-memory state.
The three handlers map to the bot's three entry points: a fresh mention in a channel, follow-up in a thread it already subscribed to, and direct messages. All three converge on the same research handler with a mode flag.
Copy link to headingParticipant policy and session state
src/lib/research-handler.ts decides whether to respond and which session to use. The participant policy is a few lines:
An unmentioned message counts as a follow-up only when the bot is talking to one person. When a second human joins, the bot unsubscribes and stays quiet until someone mentions it again.
Session resolution is validate-or-recreate. The stored session ID is untrusted input from Redis, so ownedSession() checks it before use: the session must exist, belong to this agent, and not be archived or terminated. Transient API errors are re-thrown, so a network blip doesn't discard a thread's research context. When the session really is gone, the handler creates a new one, stores it with thread.setState(), and posts a one-line note.
Every turn then runs through enqueueTurn, which serializes turns per thread within one server process.
Copy link to headingThe event-stream bridge
Most of the template's work happens in src/lib/managed-agents.ts. It sends each user turn to a session, listens to the Managed Agents event stream, and posts the agent's replies back into the Slack thread.
The stream only emits events produced after attachment, so streamTurn subscribes first and then sends the user.message event:
The returned event ID acts as an anchor. The loop discards everything on the stream until it sees its own user.message echo back, so events left over from a previous turn never produce a stale reply.
From there, the bridge streams the reply as the agent writes it. It accumulates event_delta fragments with the SDK's accumulateManagedAgentsEvent helper and pushes new text into a streaming Slack post. The buffered agent.message event that follows is authoritative, and the bridge reconciles against it, either finishing the streamed post in place or replacing it with the final text.
How a turn ends depends on the stop reason. An end_turn stop completes the turn. A requires_action stop means the agent asked for a tool approval this headless bot can't render, so it asks the user to restore the always-allow policy in a new thread. Terminated or deleted sessions get a "start a new thread" note. If the HTTP stream drops mid-turn, the bot explains that the research continues on Anthropic's side and asks the user not to resend.
The bridge also counts model requests, token usage, and web searches and fetches for the optional diagnostics card. It logs event types, tool names, and sanitized error labels, never message content or tool inputs.
Copy link to headingThe agent definition
The analyst itself lives in scripts/cma/lib/agent.ts, which sets the model, the system prompt, and the tool policy. Here's the tool policy:
Every tool in the toolset is auto-approved except Bash, which is disabled outright. Auto-approval is what makes a headless Slack bot workable, since there's no UI to click approve in. Bash comes out entirely because the agent reads untrusted web pages, and an auto-approved shell with network access would let a malicious page trick it into leaking conversation data. Don't re-enable Bash without a real human-approval flow and restricted egress.
The system prompt sets the analyst's working style. It acknowledges concrete research questions with a single short message, works silently, prefers primary sources, and dates any figure that may change. Briefs stay under 1,800 characters in a fixed structure, and follow-ups reuse prior research rather than searching again.
Copy link to headingFast webhook acknowledgement
Slack expects webhook acknowledgements within seconds, but a research turn takes minutes. The route at src/app/api/webhooks/[platform]/route.ts splits the two with Next.js's after():
Chat SDK's webhook handler acknowledges Slack right away and runs the turn in the background, where maxDuration = 300 gives it up to five minutes to finish streaming. If Slack redelivers a webhook, Redis-backed deduplication stops the turn from running twice. And because the route takes the platform as a parameter, adding another Chat SDK adapter later reuses the same file.
Copy link to headingDebug mode
Set CLAUDE_DEBUG_MODE=true to post a compact diagnostics card after every completed turn: duration, model requests, token and prompt-cache usage, web search and fetch counts, and a link to the session trace in the Claude Console.
The card is a Chat SDK JSX component in src/lib/debug-card.tsx that renders as a native Slack table, useful while tuning the prompt or tracking costs.
Copy link to headingCustomize the analyst
To change the model, system prompt, or tool policy, edit scripts/cma/lib/agent.ts and publish the changes as a new agent version:
Anthropic pins each session to the agent version that created it, so existing Slack threads keep the old behavior even after an update. Start a new thread to see your changes. Use cma:update for changes and cma:setup only for first-time provisioning; setup exits with an error if the IDs are already configured.
To switch models, set MODEL in agent.ts to another Managed Agents-supported Claude model and run the update command again. To rename the bot, set BOT_USERNAME in the environment, it defaults to claude-research-bot.
The project uses Ultracite (a Biome preset) for linting and formatting. pnpm check checks the rules, pnpm fix auto-fixes what it can, and pnpm validate runs lint, typecheck, Knip, and a full build together.
Copy link to headingEnvironment variables
| Variable | Required | Default | What it does |
|---|---|---|---|
ANTHROPIC_API_KEY | Yes | None | Authenticates Claude Managed Agents |
CLAUDE_AGENT_ID | Yes | None | Persistent analyst created by pnpm cma:setup |
CLAUDE_ENVIRONMENT_ID | Yes | None | Anthropic-managed sandbox created by setup |
REDIS_URL | Yes | None | Stores subscriptions, deduplication, and thread/session mappings |
SLACK_CONNECTOR | Yes | None | Vercel Connect Slack connector UID |
BOT_USERNAME | No | claude-research-bot | Chat SDK bot name |
CLAUDE_DEBUG_MODE | No | false | Posts per-turn diagnostics and a Claude Console link |
The required variables are validated when the server process starts, so a missing value fails the deployment fast instead of failing the first webhook.
Copy link to headingCleanup
The agent and its sandbox environment are persistent Anthropic resources that outlive your deployments. Archive them when you're finished:
Confirm both Managed Agents IDs, since archiving is permanent.
Copy link to headingTroubleshooting
Each item below lists a symptom, its cause, and the fix.
Copy link to heading@mentions don't get a response
Symptom: You @mention the bot in Slack, but it doesn't reply.
Cause: The bot isn't in the channel yet, the deployment hasn't finished, or a required environment variable is missing, which fails the server at startup.
Fix: Invite the bot to the channel, confirm the deployment succeeded, and check the deployment logs for a must be set configuration error. The deploy flow points Slack's events at /api/webhooks/slack, so the path is already correct.
Copy link to headingThe bot says earlier research context is no longer available
Symptom: A follow-up in an existing thread gets "Earlier research context for this thread is no longer available, so I'm starting a fresh session."
Cause: The stored session no longer validates. The Redis thread state expired, or the session was archived, terminated, or belongs to a different agent (for example, after re-running setup and creating a new agent ID).
Fix: Nothing is broken; the bot already created a fresh session for the thread. Restate any context the new session needs. If it happens after re-provisioning, make sure the deployed CLAUDE_AGENT_ID matches the agent your threads were created against.
Copy link to headingThe bot reports an approval it can't handle
Symptom: A turn ends with "The agent asked for an approval this bot can't handle."
Cause: The agent's tool policy no longer auto-approves a tool it tried to use. This Slack surface has no way to render an approve button, so the turn stops.
Fix: Restore the always-allow default in agentTools() in scripts/cma/lib/agent.ts, run pnpm cma:update, and start a new Slack thread. Keep Bash disabled; see the agent definition section for why.
Copy link to headingA long turn ends with a lost-connection message
Symptom: Mid-research, the bot posts "I lost my connection mid-research, but the work continues on Anthropic's side."
Cause: The HTTP event stream between your deployment and Anthropic dropped before the turn completed. The session itself keeps running.
Fix: Wait a minute or two and check the thread instead of resending. Resending queues a duplicate turn behind the one that's still running.
Copy link to headingFollow-ups in a channel are ignored
Symptom: The bot answered your first mention, but stopped responding to follow-ups in the thread.
Cause: A second human joined the thread. The participant policy unsubscribes the bot from multi-human threads so it doesn't interject in group conversation.
Fix: @mention the bot explicitly. A mention always gets a response and re-subscribes it to the thread.