Chapter 10 · 9 min read
Run it in production
Hosting, retries, logging, monitoring and when to let Anthropic host the agent with Managed Agents.
A worker that runs on your laptop is a prototype. In production it needs a home, a way to survive errors, a record of what it did, and someone who hears about it when it misbehaves.
Where it runs
| Option | You host | Good when |
|---|---|---|
| Inside your existing web app | Yes | The worker answers a chat on your site; each message is a request to your server |
| A background worker with a queue | Yes | Jobs arrive from email, WhatsApp or a form and can take a minute or two |
| Claude Managed Agents (beta) | No: Anthropic runs the loop and a sandbox per session | Long-running jobs, agents that work with files or run code, or agents that run on a schedule |
Running it yourself
- Do not make the customer wait on a long job. Reply "I'm checking that now", put the job on a queue, and send the answer when it is ready. Stream replies in a chat so text appears as it is written.
- Keep the conversation in your database, not in memory, so a restart does not lose it. Save the full response content each turn, not just the text.
- Retries: the SDK already retries rate limits and server errors twice. Set a timeout that suits the job, and decide what the customer sees if everything fails ("A person will reply by email" is a fine answer).
- One key per worker, stored in the server's environment settings, so you can see its usage and revoke it alone.
Letting Anthropic host it: Managed Agents
With Managed Agents you create an agent once (its model, job description and tools, stored and versioned) and then start a session for each job. Each session gets its own sandbox where bash, file work and code execution happen, and you follow it as a stream of events. It also supports:
- Scheduled deployments: the agent runs on a timetable ("every weekday at 8:00"), with a record of each run.
- Secrets in a vault, substituted when a request leaves the sandbox, so the agent never holds them directly.
- Permission policies: which tool calls run automatically and which wait for a person.
What it looks like in code
Create the environment and the agent once (in a setup script, never on every request), store their IDs, then start a session per job and follow its events. Your own tools still run in your code: when the agent calls one, you send the result back.
import anthropic
client = anthropic.Anthropic()
# Once, at setup: where sessions run, and the agent itself (both are stored and reused)
environment = client.beta.environments.create(
name="support-env",
config={"type": "cloud", "networking": {"type": "limited", "allow_package_managers": True, "allow_mcp_servers": True}},
)
agent = client.beta.agents.create(
name="Repair support",
model="claude-opus-5-5",
system=SYSTEM,
tools=[
{"type": "agent_toolset_20260401", # Anthropic's built-in tools, run in the sandbox
"default_config": {"permission_policy": {"type": "auto"}},
"configs": [{"name": "web_fetch", "enabled": False}, {"name": "web_search", "enabled": False}]},
{"type": "custom", "name": "get_repair_status", # your own tool, run by your code
"description": "Look up a repair by its job number.",
"input_schema": {"type": "object", "properties": {"job_number": {"type": "string"}}, "required": ["job_number"]}},
],
)
# For each job: a session, a message, and the event stream
session = client.beta.sessions.create(
agent={"type": "agent", "id": agent.id, "version": agent.version},
environment_id=environment.id,
)
with client.beta.sessions.events.stream(session_id=session.id) as stream: # open the stream first
client.beta.sessions.events.send(session_id=session.id, events=[
{"type": "user.message", "content": [{"type": "text", "text": "Is FD-10482 ready?"}]}])
for event in stream:
if event.type == "agent.message":
print("".join(b.text for b in event.content if b.type == "text"), end="")
elif event.type == "agent.custom_tool_use": # the agent called your tool: run it, send the result
client.beta.sessions.events.send(session_id=session.id, events=[{
"type": "user.custom_tool_result", "custom_tool_use_id": event.id,
"content": [{"type": "text", "text": run_my_tool(event.name, event.input)}]}])
elif (event.type in ("agent.tool_use", "agent.mcp_tool_use")) and event.evaluated_permission == "ask":
client.beta.sessions.events.send(session_id=session.id, events=[{ # paused for a decision; deny when nobody is watching
"type": "user.tool_confirmation", "tool_use_id": event.id, "result": "deny"}])
elif event.type == "session.status_idle" and event.stop_reason.type != "requires_action":
break
elif event.type == "session.status_terminated":
break
Web search and web fetch are switched off here on purpose: a support agent should answer from your systems, not the open web. Each session also has a trace page in the Claude Console, which is the quickest way to see exactly what it did.
It is a good fit for agents that work for minutes or hours on one task. For a chat that answers in seconds inside your own app, the tool runner on your own server is usually simpler.
Know what it did
- Log every turn: the request ID (
response._request_id), the tools called and their inputs, the stop reason, the tokens used and the time taken. - Log decisions, not just words: "booked slot 14:30 for job FD-10482" is what you will search for later.
- Traces: the SDKs support OpenTelemetry, if you already use a monitoring service that reads it.
- A weekly read: twenty random conversations, read by a person, every week. It is the cheapest quality check there is.
Alerts worth having
- Spend per day above a set amount.
- A jump in hand-overs to staff (something upstream broke, or customers are asking about something new).
- A jump in refusals, errors or conversations that hit the turn limit.
- A tool failing repeatedly (your booking system is down; the worker should say so, not guess).
Changing it safely
- Change one thing: the job description, a tool, the model or the effort level.
- Run the eval from the last chapter. Compare with the previous score.
- Release it to a share of conversations first, if you can, and watch the alerts.
- Keep the previous version ready to switch back to.
When a newer Claude model comes out, treat it the same way: run the eval, read the failures, adjust the job description, then switch. Prompts written for an older model are often more prescriptive than a newer one needs.