---
title: Build Claude Managed Agents with Chat SDK
description: Ship a Slack research bot built on Claude Managed Agents and Chat SDK. One persistent session per thread, streamed briefs with sources, and no Slack credentials to manage.
url: /kb/guide/claude-managed-agents-chat-sdk
canonical_url: "https://vercel.com/kb/guide/claude-managed-agents-chat-sdk"
published: 2026-07-24
last_updated: 2026-08-24
authors: Ben Sabic
related:
  - /docs/connect
  - /docs/cli
  - /docs/cli/connect
install_vercel_plugin: npx plugins add vercel/vercel-plugin
---
<!-- docsgraph:related -->
## Related pages

> **For AI agents:** Follow these links to understand how this page connects to the rest of the Vercel ecosystem. For the full cross-link map (inbound, outbound, prerequisites, and semantic neighbors), see the .graph.md link below.

- [Run Claude Managed Agents with Chat SDK](https://vercel.com/changelog/claude-managed-agents-with-chat-sdk?from=related&source_path=%2Fkb%2Fguide%2Fclaude-managed-agents-chat-sdk&source_site=vercel-kb&relationship=related)
- [Claude Code and Claude Agent SDK](https://vercel.com/docs/ai-gateway/coding-agents/claude-code?from=related&source_path=%2Fkb%2Fguide%2Fclaude-managed-agents-chat-sdk&source_site=vercel-kb&relationship=related) — Use Claude Code and the Claude Agent SDK with AI Gateway.
- [Anthropic Messages API](https://vercel.com/docs/ai-gateway/sdks-and-apis/anthropic-messages-api?from=related&source_path=%2Fkb%2Fguide%2Fclaude-managed-agents-chat-sdk&source_site=vercel-kb&relationship=related) — Use the Anthropic Messages API with AI Gateway for seamless integration with Anthropic SDK tools.
- [Building Slack agents can be easy](https://vercel.com/blog/building-slack-agents-can-be-easy?from=related&source_path=%2Fkb%2Fguide%2Fclaude-managed-agents-chat-sdk&source_site=vercel-kb&relationship=related)
- [Build Claude Managed Agents with Vercel Services](https://vercel.com/kb/guide/claude-managed-agents-vercel-services?from=related&source_path=%2Fkb%2Fguide%2Fclaude-managed-agents-chat-sdk&source_site=vercel-kb&relationship=related) — Deploy Claude Managed Agents with Vercel Services to run a streaming research analyst in a browser chat, using Anthropic
- [How to build an AI agent for Slack with Chat SDK and AI SDK](https://vercel.com/kb/guide/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk?from=related&source_path=%2Fkb%2Fguide%2Fclaude-managed-agents-chat-sdk&source_site=vercel-kb&relationship=related) — Build a Slack AI agent using Chat SDK, AI SDK's ToolLoopAgent, and Vercel AI Gateway. Covers project setup, tool definit
- [Ship social posts from Slack with eve and Typefully](https://vercel.com/kb/guide/eve-typefully-social-media-agent?from=related&source_path=%2Fkb%2Fguide%2Fclaude-managed-agents-chat-sdk&source_site=vercel-kb&relationship=related) — A Slack-based social media agent built on eve. It drafts posts and threads for X, LinkedIn, Threads, Bluesky, and Mastod
- [Build your first Slack agent with eve](https://vercel.com/kb/guide/eve-slack-agent-starter?from=related&source_path=%2Fkb%2Fguide%2Fclaude-managed-agents-chat-sdk&source_site=vercel-kb&relationship=related) — Deploy the eve Slack agent template: a starter Slack bot built on the eve framework with an example tool and skill.
- [Draft content in your voice from Slack with eve](https://vercel.com/kb/guide/eve-content-agent?from=related&source_path=%2Fkb%2Fguide%2Fclaude-managed-agents-chat-sdk&source_site=vercel-kb&relationship=related) — Deploy the eve content agent template, a Slack bot that drafts blog posts, LinkedIn posts, release notes, and newsletter

Full cross-link map for this page: [/kb/guide/claude-managed-agents-chat-sdk.graph.md](/kb/guide/claude-managed-agents-chat-sdk.graph.md?from=related&source_path=%2Fkb%2Fguide%2Fclaude-managed-agents-chat-sdk&source_site=vercel-kb&relationship=graph)
<!-- /docsgraph:related -->


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](https://platform.claude.com/docs/en/managed-agents/overview) session, so follow-ups retain the thread's prior research context. The bot is built with [Chat SDK](https://chat-sdk.dev), the unified TypeScript SDK for building chat bots

[Vercel Connect](https://vercel.com/docs/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.

## The stack

| Layer   | Choice                                                                                                    |
| ------- | --------------------------------------------------------------------------------------------------------- |
| Surface | Slack via [Chat SDK](https://chat-sdk.dev/) and [Vercel Connect](https://vercel.com/connect)              |
| Agent   | [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) with Claude Sonnet 5 |
| State   | [Upstash Redis](https://vercel.com/marketplace/upstash)                                                   |
| 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.

## Setup and deployment

### What you need before deploying

You need three accounts to deploy and run the analyst:

- A [Vercel account](https://vercel.com/signup).
  
- A Slack workspace where you can install an app.
  
- An [Anthropic API key](https://platform.claude.com/settings/workspaces/default/keys) with Managed Agents access.
  

For local development, you also need Node.js 24+, pnpm, and the [Vercel CLI](https://vercel.com/docs/cli).

### Deploy 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 in `SLACK_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](https://platform.claude.com/dashboard) 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.

### Set up from a clone

If you'd rather provision everything manually, start from the template repository:

```bash
git clone https://github.com/vercel-labs/cma-chat-sdk
cd cma-chat-sdk
pnpm install
```

Link your Vercel project, create the Slack connector with the [Vercel CLI](https://vercel.com/docs/cli/connect), and then add Upstash Redis from the Vercel Marketplace:

```bash
vercel link
vercel connect create slack --name claude-research-analyst --triggers
vercel connect attach slack/claude-research-analyst \
  --project your_vercel_project_here \
  --environment production \
  --triggers \
  --trigger-path /api/webhooks/slack

vercel integration add upstash/upstash-kv
```

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:

```bash
pnpm cma:setup --vercel
```

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:

```javascript
vercel env add ANTHROPIC_API_KEY
vercel deploy --prod
```

### Invite 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.

## How 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.

1. **@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.
   
2. **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.
   
3. **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.
   
4. **Queue the turn**: Turns within a thread run one at a time, so rapid messages can't interleave their replies.
   
5. **Research in the sandbox**: Claude Sonnet 5 plans, searches for, and fetches sources within the secure sandbox environment.
   
6. **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.
   

## Code walkthrough

The runtime is six files under `src/`, plus a small CLI under `scripts/cma/` for managing the persistent Anthropic resources.

### The Slack surface

The whole chat surface is one file, `src/lib/bot.ts`:

```typescript
import { createSlackAdapter } from "@chat-adapter/slack";
import { createRedisState } from "@chat-adapter/state-redis";
import { connectSlackAdapter } from "@vercel/connect/chat";
import { Chat } from "chat";
import { config } from "./config";
import { handleResearchMessage, type ThreadState } from "./research-handler";

const adapters = {
  slack: createSlackAdapter({
    ...connectSlackAdapter(config.slackConnector),
  }),
};

export const bot = new Chat<typeof adapters, ThreadState>({
  adapters,
  concurrency: "concurrent",
  state: createRedisState({ url: config.redisUrl }),
  userName: config.botUsername,
});

bot.onNewMention(async (thread, message) => {
  await thread.subscribe();
  await handleResearchMessage(thread, message, "new-mention");
});

bot.onSubscribedMessage(async (thread, message) => {
  await handleResearchMessage(thread, message, "subscribed");
});

bot.onDirectMessage(async (thread, message) => {
  await handleResearchMessage(thread, message, "direct");
});
```

`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.

### Participant 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:

```typescript
if (mode === "subscribed" && !message.isMention) {
  const participants = await thread.getParticipants();
  if (participants.length !== 1) {
    await thread.unsubscribe();
    return;
  }
}
```

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.

### The 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](https://platform.claude.com/docs/en/managed-agents/events-and-streaming), 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:

```typescript
const stream = await client.beta.sessions.events.stream(sessionId, {
  event_deltas: ["agent.message"],
});

const sent = await client.beta.sessions.events.send(sessionId, {
  events: [
    {
      content: [{ text, type: "text" }],
      type: "user.message",
    },
  ],
});

anchorId = sent.data?.find((event) => event.type === "user.message")?.id;
```

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.

### The 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:

```typescript
export function agentTools(): NonNullable<AgentCreateParams["tools"]> {
  return [
    {
      configs: [{ enabled: false, name: "bash" }],
      default_config: {
        enabled: true,
        permission_policy: { type: "always_allow" },
      },
      type: "agent_toolset_20260401",
    },
  ];
}
```

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.

### Fast 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()`:

```typescript
return handler(request, {
  waitUntil: (task) => after(() => task),
});
```

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.

### Debug 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.

## Customize 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:

```bash
pnpm cma:update
```

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](https://www.ultracite.ai/) (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.

## Environment 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.

## Cleanup

The agent and its sandbox environment are persistent Anthropic resources that outlive your deployments. Archive them when you're finished:

```bash
pnpm cma:archive
```

Confirm both Managed Agents IDs, since archiving is permanent.

## Troubleshooting

Each item below lists a symptom, its cause, and the fix.

### @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.

### The 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.

### The 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.

### A 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.

### Follow-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.

## Related resources

- [Claude Research Analyst template](https://github.com/vercel-labs/cma-chat-sdk)
  
- [Claude Managed Agents overview](https://platform.claude.com/docs/en/managed-agents/overview)
  
- [Claude Managed Agents events and streaming](https://platform.claude.com/docs/en/managed-agents/events-and-streaming)
  
- [Chat SDK documentation](https://chat-sdk.dev/docs) and the [Slack adapter](https://chat-sdk.dev/adapters/official/slack)
  
- [Vercel Connect](https://vercel.com/docs/connect) and [Vercel Connect CLI](https://vercel.com/docs/cli/connect)
  
- [Upstash Redis on the Vercel Marketplace](https://vercel.com/marketplace/upstash)