Build a Production-Ready Agent with Claude Managed Agents
A hands-on walkthrough of Claude Managed Agents: defining agents and environments, creating sessions, streaming events, using outcomes for iterative self-verification, and monitoring live runs in the developer console.
This lesson is original educational writing based on this video by Anthropic (published May 21, 2026). All credit for the original content goes to the creators.
1. What Claude Managed Agents is (and why it exists)
Building a production-quality AI agent from scratch is a surprisingly long engineering checklist: you need an agent loop, remote hosting, context management, state recovery, a sandboxing fleet, durable event storage, MCP server integration, and secure end-user credential injection. That is weeks of infrastructure before you write a single line of product logic.
Claude Managed Agents is a set of API endpoints β available today with any Anthropic API key β that delivers all of that infrastructure as primitives you can compose. You pick the ones you need, discard the rest, and build your own product experience on top.
The four core primitives are:
| Primitive | What it is |
|---|---|
| Agent | A reusable template: system prompt, tools, MCP servers, permission rules |
| Environment | A sandbox template: network access, pre-installed packages, self-hosted infra |
| Session | A running conversation tied to an agent + environment |
| Events | The bidirectional stream of everything happening inside a session |
2. Defining an agent and an environment
An agent definition is a reusable template you create once and reference by ID in every session. Think of it the way you think of a Docker image: you define it, version it, and then run as many instances of it as you need.
The key configuration decisions when defining an agent are:
- System prompt β the persona, rules, and context that govern every session.
- Tools β which built-in tools Claude can call (bash, file read/write, web search, computer use). Omitting a tool is the safest way to prevent a whole category of risk.
- MCP servers β external capabilities, such as a Linear MCP or a Figma MCP.
- Permission controls β per-tool policy:
auto_executefor low-risk reads,require_approvalfor anything that touches a database or runs shell commands. This is how you implement human-in-the-loop without writing custom middleware.
// Create an agent definition (one-time setup)
const agent = await client.beta.agents.create({
name: 'deal-desk-coordinator',
model: 'claude-opus-4-7',
system_prompt: `You are a senior M&A analyst assistant. When asked to evaluate
a company, delegate research tasks to specialist sub-agents and synthesize
their findings into a structured investment memo.`,
tools: [
{ type: 'web_search' },
{ type: 'file_read', policy: 'auto_execute' },
{ type: 'bash', policy: 'require_approval' },
],
mcp_servers: [
{ name: 'linear', url: 'https://mcp.linear.app/sse' },
],
});
console.log(agent.id); // save this β you'll pass it to every session
An environment defines the sandbox template: whether the container has internet access, what packages come pre-installed, and β since the launch of self-hosted environments β whether the container runs on Anthropic infrastructure or on your own Cloudflare, Modal, or Vercel fleet. Self-hosted environments let sensitive data stay inside your own network perimeter.
const env = await client.beta.environments.create({
name: 'deal-desk-sandbox',
network_access: true,
preinstall: {
npm: ['xlsx', 'pdf-parse'],
pip: ['pandas', 'openpyxl'],
},
});
3. Creating a session and listing sessions
A session is a running conversation bound to a specific agent and environment. Creating one
is equivalent to opening a new Claude.ai chat or launching claude in your terminal β except it
runs remotely, persists durably, and is fully API-driven.
At session-create time you can also attach GitHub repositories or files that should be pre-loaded into the container before Claude starts working.
// Create a session
const session = await client.beta.sessions.create({
agent_id: agent.id,
environment_id: env.id,
memory_store_ids: [memoryStore.id], // optional: give Claude persistent memory
credential_vault_ids: [vault.id], // optional: inject MCP auth tokens securely
resources: [
{
type: 'github_repository',
repo: 'acme/deal-data',
branch: 'main',
},
],
});
console.log(session.id); // pass to your UI
Listing existing sessions lets you build a sidebar or dashboard over your agentβs history:
// List all sessions for an agent
const { data: sessions } = await client.beta.sessions.list({
agent_id: agent.id,
limit: 50,
});
Sessions carry status information (running, idle, error, terminated) so you can show users a live indicator without polling the event stream.
4. Sending events and streaming responses
The event stream is the heartbeat of a session. Your application submits inbound events (user messages, interrupts, tool results, outcome definitions) and receives outbound events (Claudeβs replies, tool calls, multi-agent coordination, lifecycle changes).
Sending a user message
await client.beta.sessions.events.create(session.id, {
type: 'user',
content: [{ type: 'text', text: 'Give me a quick read on Acme Robotics.' }],
});
Streaming the response
Use the streaming endpoint to forward events to your frontend in real time. The response is a server-sent event (SSE) stream β each chunk is one typed event object.
const stream = await client.beta.sessions.events.stream(session.id);
for await (const event of stream) {
switch (event.type) {
case 'agent.message':
// Claude sent text β stream to your UI
process.stdout.write(event.content);
break;
case 'agent.tool_call':
// Claude is calling a tool β show a spinner
console.log(`[tool] ${event.tool_name}`, event.input);
break;
case 'agent.subagent_spawned':
// A sub-agent was launched β update your multi-agent view
console.log(`[subagent] ${event.subagent_id} started`);
break;
case 'session.error':
console.error('Session error:', event.error);
break;
case 'session.terminated':
console.log('Session finished');
break;
}
}
Sending an interrupt
If Claude heads in the wrong direction you can cut it off without terminating the session:
await client.beta.sessions.events.create(session.id, {
type: 'interrupt',
reason: 'Stop β please re-read the brief before continuing.',
});
5. Outcome definitions β making Claude self-verify
The most powerful inbound event type is outcome. Instead of just asking Claude a question, you hand it a rubric β a structured spec or checklist β and Claude enters a loop where it:
- Researches and drafts an answer.
- Evaluates its own output against the rubric.
- Iterates until it is confident the rubric is satisfied.
- Emits an
outcome.completedevent with its findings.
This is especially useful for multi-agent workflows where individual sub-agents surface partial results and a coordinator must synthesize them into something that meets a quality bar.
// Send an outcome definition instead of a plain user message
await client.beta.sessions.events.create(session.id, {
type: 'outcome',
content: `
Evaluate the following three acquisition targets and produce an investment memo.
## Companies
- Bridgewell Dynamics
- Norwood Automation
- Acme Robotics
## Rubric β your response must satisfy ALL of these:
- [ ] Financial summary for each company (revenue, growth, EBITDA margin)
- [ ] Competitive positioning relative to each other
- [ ] Top 3 risks per company, with severity rating
- [ ] A ranked recommendation with clear reasoning
Iterate until you can honestly confirm every rubric item is addressed.
`,
});
When you listen to the event stream after sending an outcome you will see
outcome.processing_started followed by several iterations of tool calls and self-critique,
and finally outcome.completed once Claude is satisfied.
6. Monitoring and memory in the developer console
The developer console gives you a live visual debugger for every running session. Navigate to Sessions β [your session] and you will see:
- Agent timeline β one horizontal lane per agent thread (coordinator + sub-agents), updated in real time.
- Event inspector β click any tool call to see exact inputs and outputs.
- Duration indicators β flag tool calls that are taking unusually long so you can diagnose slow MCP servers or oversized files.
Beyond real-time monitoring, the console exposes:
- Memory stores β a key/value store Claude reads and writes across sessions. You can edit individual memories directly if Claude recorded something incorrectly, or seed memories before the first session runs.
- Credential vaults β store MCP auth tokens once; Anthropic injects them when needed without the tokens ever entering Claudeβs context window.
- Environments tab β shows self-hosted sandbox registrations and their current status.
- Agent versions β roll back to any previous agent definition version if a system prompt change regresses quality.
Check your understanding
5 questions Β· your answers are saved in this browser only
-
1. Which Claude Managed Agents primitive defines the sandbox template (network access, pre-installed packages, and hosting provider)?
-
2. What is the primary purpose of a per-tool `require_approval` permission policy?
-
3. What happens when you send an `outcome` event to a session instead of a plain `user` event?
-
4. What is the purpose of `span.*` events in the event stream?
-
5. How do credential vaults improve MCP server security compared to passing tokens directly in the session payload?
Build it yourself
Follow these exact steps to reproduce it yourself Β· estimated time: ~30 min
Prerequisites
- Anthropic API key with Managed Agents access
- Node.js 18+ or Bun
- A Linear account (optional, for the MCP demo)
Step 1 β Install the SDK and scaffold a project
mkdir deal-desk-agent && cd deal-desk-agent
bun init -y # or: npm init -y
bun add @anthropic-ai/sdkCreate a .env file:
ANTHROPIC_API_KEY=sk-ant-...Step 2 β Create an agent definition
// src/setup.ts
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
async function setup() {
// Create the agent
const agent = await client.beta.agents.create({
name: 'deal-desk-coordinator',
model: 'claude-opus-4-7',
system_prompt: `You are a senior M&A analyst. When asked to evaluate
acquisition targets, delegate to specialist sub-agents and synthesize their
findings into a structured investment memo.`,
tools: [
{ type: 'web_search' },
{ type: 'file_read', policy: 'auto_execute' },
{ type: 'bash', policy: 'require_approval' },
],
});
// Create the environment
const env = await client.beta.environments.create({
name: 'deal-desk-sandbox',
network_access: true,
});
console.log('Agent ID:', agent.id);
console.log('Environment ID:', env.id);
// Save these IDs β paste into your .env file
}
setup();Run it once:
bun run src/setup.tsAdd the printed IDs to .env:
AGENT_ID=agt_...
ENVIRONMENT_ID=env_...Step 3 β Create a session
// src/session.ts
import Anthropic from '@anthropic-ai/sdk';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
export async function createSession() {
const session = await client.beta.sessions.create({
agent_id: process.env.AGENT_ID!,
environment_id: process.env.ENVIRONMENT_ID!,
});
console.log('Session created:', session.id);
return session;
}Step 4 β Send a message and stream the response
// src/chat.ts
import Anthropic from '@anthropic-ai/sdk';
import { createSession } from './session';
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
async function chat(sessionId: string, text: string) {
// Send the user message
await client.beta.sessions.events.create(sessionId, {
type: 'user',
content: [{ type: 'text', text }],
});
// Stream the response
const stream = await client.beta.sessions.events.stream(sessionId);
for await (const event of stream) {
if (event.type === 'agent.message') {
process.stdout.write(event.content);
} else if (event.type === 'agent.tool_call') {
console.log(`\n[tool] ${event.tool_name}`);
} else if (event.type === 'session.terminated') {
break;
} else if (event.type === 'session.error') {
console.error('\nError:', event.error);
break;
}
}
}
(async () => {
const session = await createSession();
await chat(session.id, 'Give me a quick overview of the robotics industry.');
})();bun run src/chat.tsStep 5 β Upgrade to an outcome for self-verified research
Replace the plain user message with an outcome definition so Claude iterates until it satisfies a quality rubric:
await client.beta.sessions.events.create(session.id, {
type: 'outcome',
content: `
Research Acme Robotics and produce an investment memo.
## Rubric
- [ ] Revenue and growth rate (last 2 years)
- [ ] Top 3 competitors and differentiation
- [ ] Key risks with severity (High / Medium / Low)
- [ ] Go / No-go recommendation with one-paragraph rationale
Iterate until every rubric item is fully addressed.
`,
});Then stream as before β watch for outcome.processing_started and outcome.completed
events in addition to the regular agent messages.
Step 6 β Monitor in the developer console
- Go to console.anthropic.com β Managed Agents β Sessions.
- Find your session (it shows up by ID and creation time).
- Click into it to see the live agent timeline.
- Expand any tool call to inspect its exact input and output.
- If Claude wrote a memory, navigate to Memory Stores to review or edit it.