Claude Managed Agents gives you a fully managed agent runtime. Claude can read files, run commands, browse the web, and call MCP servers without you building the agent loop yourself.
This guide builds an internal knowledge agent on Vercel where your team can ask questions across GitHub, Notion, and Slack. It covers Sign in with Vercel for auth, a durable Workflow that polls and streams agent events in real-time via SSE, and per-user credential vaults so the agent can call MCP servers on behalf of each user.

You can try the live demo, browse the source, or continue following along below to understand how everything works from the ground up.
Before diving into code, here's the full data flow:
- User opens a new chat. The client calls the session API, which creates an Anthropic session and starts a long-lived Workflow run. The user's first message is passed directly to the workflow as input.
- Workflow sends the message. A
"use step"function forwards the text to Anthropic viasessions.events.send. - Workflow polls and streams. Another step fetches new events from the Anthropic events API and writes them to the workflow's durable stream via
getWritable(). It sleeps 3 seconds between polls, releasing the function instance each time. - Client receives events. An
EventSourceconnection to/api/readable/${runId}delivers events in real-time as they're written. - Turn completes. When a terminal event arrives (like
session.status_idle), polling stops and the workflow pauses at adefineHook, waiting for the next message. - User sends a follow-up. The message API calls
messageHook.resume()to wake the workflow. Same run, same durable stream. - Page refresh.
getReadable()replays all events from the beginning, so the client gets full history without a database query.
The workflow run is both the execution engine and the event log. The only database table is managed_agent_session for metadata like title and user ownership.
Install Neon Postgres from the Vercel Marketplace, then pull the provisioned DATABASE_URL into your local .env.local:
Wrap your Next.js config with the Workflow DevKit so the "use workflow" and "use step" directives work:
After vercel env pull, your .env.local already has DATABASE_URL. Add the remaining variables:
| Variable | Where to get it |
|---|---|
ANTHROPIC_API_KEY | platform.claude.com/settings/keys |
ANTHROPIC_AGENT_ID | Anthropic console, after creating a Managed Agent |
ANTHROPIC_ENVIRONMENT_ID | Same place as above |
VERCEL_CLIENT_ID | Sign in with Vercel OAuth app |
VERCEL_CLIENT_SECRET | Same place as above |
TOKEN_ENCRYPTION_KEY | openssl rand -hex 32 |
BETTER_AUTH_SECRET | openssl rand -base64 32 |
One table for session metadata. Agent events live in the workflow run, not the database:
A thin wrapper around the SDK:
And the session creation helper. It accepts an array of vault IDs so the agent can access per-user MCP credentials (covered in Credential vaults & MCP):
This is the core of the integration. Managed Agents are asynchronous: after you send a message, the agent processes it and emits events over time. A durable Workflow polls the events API and streams results to the client. No database writes needed.
Three primitives from the workflow package make this work:
defineHookpauses the workflow until resumed from outside (used for receiving follow-up messages)sleepreleases the function instance between polls instead of burning compute withsetTimeoutgetWritablewrites events to a durable stream that clients read via SSE
The messageHook is exported so the message API route can call messageHook.resume() to wake up the workflow.
A "use step" function forwards the user's text to Anthropic:
Another step fetches new events and writes them to the workflow's durable stream:
Each writer.write() call durably persists the event and pushes it to any connected SSE clients. The lastEventId tracking ensures only new events are processed across polls.
Ties everything together. The initial message is processed directly, then the workflow enters a hook loop for follow-ups:
Why is the first message passed as initialMessage instead of going through the hook? On Vercel, start() returns the run ID before the workflow has executed to the point where messageHook.create() runs. If the client immediately called resume(), the hook wouldn't exist yet. Baking the first message into the workflow input sidesteps this race condition entirely.
The for await (const { text } of hook) pattern receives each resume() call as a new iteration. The hook is created once with a deterministic token, and the workflow pauses between turns. When messageHook.resume("msg:${sessionId}", { text }) is called, the workflow wakes up, processes the turn, then pauses again.
sleep() between polls releases the function instance. If the function crashes, the Workflow SDK replays from the last completed step. The lastEventId carries across turns, so follow-up messages only fetch new events.
The client reads events from the workflow through a Server-Sent Events endpoint:
getRun(runId).getReadable() replays all events from the beginning, then keeps the connection open for new ones. On page refresh, the client reconnects and gets full history automatically.
The session route creates an Anthropic session, starts the workflow with the user's first message, and returns the run ID so the client can connect to the event stream:
The first message is part of the session creation request. The workflow starts processing it immediately, no separate message send needed.
For subsequent messages, the route resumes the workflow's hook:
No start() call here. The workflow was already started when the session was created. messageHook.resume() wakes it up with the new text, using the deterministic token msg:${sessionId} to find the right hook.
The chat panel connects to the SSE stream on mount and receives events in real-time:
The EventSource closes when the workflow pauses between turns (the readable stream ends). On follow-ups, handleSend reconnects it. The seenIdsRef deduplicates events that get replayed on reconnection. The optimistic user message appears immediately and is replaced when the real event arrives.
Vaults let you register per-user credentials and reference them by ID at session creation. Each user gets a vault, and you sync MCP tokens into it so the agent can authenticate with external services.
The static_bearer type tells Anthropic to inject this token as a Bearer header when the agent connects to the specified MCP server URL. Your application never passes tokens directly to the agent.
Managed Agents supports MCP servers as first-class tools. The pattern for each integration:
- User completes OAuth with the MCP provider (GitHub, Notion, Slack)
- Store the encrypted access token in your database
- On session creation, sync the token into the user's vault
- The agent authenticates automatically when calling that MCP server
You configure which MCP servers the agent has access to in the Anthropic console as part of the agent's tool configuration.
Deploy the template in one click, or give this guide (or the project's SPEC.md) directly to your coding agent.
- Add more MCP servers: Connect Linear, Jira, or custom internal tools by registering their MCP URLs in the agent config and implementing the OAuth flow
- Stream responses: The Anthropic API supports SSE streaming for even lower latency than polling the events list endpoint
- Custom tools: Define custom tools to extend the agent with your own business logic
- Multi-agent: Use multi-agent orchestration to coordinate specialized agents
For more on the Managed Agents API, see the official documentation.