mission://guides/claude-ai-worker/run-it-in-production
Book a call

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

OptionYou hostGood when
Inside your existing web appYesThe worker answers a chat on your site; each message is a request to your server
A background worker with a queueYesJobs 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 sessionLong-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

  1. Change one thing: the job description, a tool, the model or the effort level.
  2. Run the eval from the last chapter. Compare with the previous score.
  3. Release it to a share of conversations first, if you can, and watch the alerts.
  4. 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.

Want this built for you?

I build AI workers on Claude for businesses in the UK, US and UAE.

Book a free call