Building AI Agents That Run Overnight, Not Just in Demos

The first time I left an agent running unattended, I woke up to a pipeline that had politely retried the same malformed request about four hundred times, burned through a chunk of budget, and produced nothing. The model was fine. The system around it was not. That gap, between an agent that works when you are watching and one that works when you are asleep, is almost entirely engineering, and it is what this post is about.
I run autonomous agents in production: a multi-agent content and SEO machine that researches, writes, optimizes and publishes across several sites, and a self-learning loop that reads search performance and decides what to produce next. None of it has a human in the loop at 3 a.m. Here are the patterns that made that possible.
Start with the simplest thing that could work
Anthropic's guidance on agents is to find the simplest solution possible and add complexity only when needed, and it warns that autonomy brings higher costs and the potential for compounding errors (Building Effective AI Agents). I would put it more bluntly: every degree of freedom you give an agent is a degree of freedom for it to fail at night.
My rule is that if the steps are known in advance, it is a workflow, not an agent. A fixed sequence of LLM calls with code between them is cheaper, faster, and debuggable. I reach for a real agent loop only where the path genuinely depends on what the model discovers along the way. In my content system, "research a topic" is an agent (the number of searches and follow-ups is unknowable up front). "Publish this finished article to the CMS" is plain code with a validation step. Mixing those up is the most common architecture mistake I see.
Decompose the task until each step is checkable
A task is ready to run unattended when every step has an output you can verify without asking a human. That is the whole test.
When I decompose, I ask three questions of each step:
- What is the artifact? A step must produce something concrete (a JSON object, a row, a file), not "a decision".
- How is it validated in code? Schema check, length bounds, link resolution, a deterministic assertion. If the only validator is another LLM, I treat the step as higher risk and add a second line of defense.
- What happens if it fails? Retry, skip, fall back, or stop the whole run. This must be decided at design time, not discovered at runtime.
Here is the shape of a step in my pipelines, stripped down:
@dataclass
class Step:
name: str
run: Callable[[Context], Artifact]
validate: Callable[[Artifact], list[str]] # returns list of problems
max_attempts: int = 3
on_exhausted: Literal["skip", "halt", "fallback"] = "halt"
The important field is on_exhausted. Most demos have no answer to "what if it never succeeds." Production needs one for every step. For the content pipeline, a failed research step halts that article but never the whole batch. A failed publish step halts everything for that site and pages me, because a half-published state is worse than no state.
Design tools like an API for a very literal colleague
Tool design deserves more of your time than prompt tuning. Anthropic reports spending more time optimizing tools than the overall prompt when building their SWE-bench agent, and one fix was changing a tool to always require absolute filepaths, which eliminated the model's mistakes with relative paths (source). That is poka-yoke: make the wrong call impossible instead of asking the model to be careful.
I have found the same thing repeatedly. A few principles that have paid off:
- Write long descriptions. The Claude docs call detailed tool descriptions the most important factor in tool performance and recommend at least 3 to 4 sentences per tool (Define tools). Say what it does, when to use it, when not to, and what each parameter means.
- Consolidate and namespace. The same docs recommend fewer, consolidated tools and namespaced names like
github_list_prs. Twenty near-identical tools is a selection problem you are creating for the model. - Return high-signal output. Do not dump a 40KB API response into context. Return the fields the next step needs. This cuts cost and cuts the odds the model latches onto irrelevant noise.
- Make errors instructive. When a tool fails, the message the model sees should say what was wrong and what a valid call looks like. "Invalid input" produces a retry of the same bad call. "slug must be lowercase kebab-case, got 'My Post'" produces a fix.
- Enforce the schema. Setting
strict: trueon tool definitions enables schema validation of tool inputs, and combined withtool_choiceset toanyit guarantees both that a tool is called and that the inputs follow the schema (Implement tool use).
One gotcha worth knowing: when tool_choice is any or tool, the API prefills the assistant message, so the model emits no natural-language explanation before the tool call, even if asked to (docs). I use forced tool calls for extraction-style steps where I want structure and no chatter, and leave auto for steps where reasoning-out-loud helps. Also check the live docs for which models support forced tool use before you depend on it, since that has varied between doc versions.
Retries: bound everything, retry the right layer
Retries are where unattended agents quietly bankrupt you or silently loop. I separate three layers, because they fail differently:
| Layer | Failure example | My response |
|---|---|---|
| Transport | 429, 5xx, timeout | Exponential backoff with jitter, small fixed cap |
| Model output | Truncated response, invalid tool args | Re-run the turn, adjusting parameters (e.g. more max_tokens) |
| Task logic | Output parses but fails validation | Retry with the validation errors fed back, then give up |
Anthropic's SDK tool runner supports per-turn retries and parameter changes such as bumping max_tokens to re-run a truncated turn, and the whole loop can be bounded with max_iterations (tool-use concepts). That second part is the one people skip. Every loop needs a hard ceiling, in iterations, in wall-clock time, and in spend. My 400-retry morning happened because I had a retry policy but no budget.
Two more habits. First, feed the validation errors back into the retry instead of re-sending the identical prompt; an identical prompt tends to produce an identical failure. Second, make steps idempotent so a retry after a crash cannot publish twice. I key every side-effecting action on a deterministic ID and check before writing.
Observability: you cannot debug a run you cannot replay
When something breaks at night, the question is always the same: what did the agent see, decide and call, in order? If you cannot answer that from logs, you will rerun and hope.
OpenAI's agent-evals guide describes a trace as the end-to-end record of model calls, tool calls, guardrails and handoffs for one run, and recommends trace grading to find regressions and failure modes at scale (Evaluate agent workflows). I agree with the framing even though my stack is mixed. Whatever tooling you use, the unit of observability is the run, not the individual LLM call.
What I record for every run:
- The full ordered sequence of model calls and tool calls, with inputs and outputs
- Per-step attempt counts and the validation errors that triggered retries
- Token usage and cost per step, rolled up per run
- A terminal status: succeeded, skipped, halted, or budget-exceeded
A few practical notes from the docs. OpenTelemetry's GenAI conventions say that when a request is retried automatically after a transient issue, the span should cover the logical operation including all retries (spec), which is the right mental model: one logical step, many attempts. They also include a dedicated document for agent operations separate from model-operation spans (README). I would treat the specific span names as still evolving and verify against the current spec before standardizing on them.
Two operational traps. In long-running workers, flush traces at the end of each unit of work if you need delivery guarantees; the OpenAI Agents SDK docs call this out for worker processes like Celery tasks (tracing doc). And mind what you capture: in that SDK, trace_include_sensitive_data defaults to True, so prompts and tool inputs/outputs land in traces unless you turn it off (Tracing). For an unattended agent touching customer data, decide that deliberately.
Guardrails: the boring controls that let you sleep
Guardrails are not one clever filter. They are a stack of dull, deterministic limits around a probabilistic core. The ones I never ship without:
- Budgets. Max iterations, max wall-clock time, max spend per run and per day. Exceeding any one halts the run and alerts me.
- Least-privilege tools. The research agent can read the web and write to a scratch table. It cannot publish. Publishing is a separate, deterministic step that only accepts validated artifacts.
- Output gates before side effects. Anything irreversible (publishing, sending, deleting, paying) passes through code-level validation first, never directly from model output.
- A kill switch and a dry-run mode. I can stop every worker with one flag, and any pipeline can run end to end with side effects stubbed out.
- Sandboxing. Anthropic recommends extensive testing in sandboxed environments along with appropriate guardrails (source); I extend that to running anything high-risk in an isolated environment even in production.
- Alerts on absence. The scariest failure is not an error, it is a run that silently did not happen. I alert when an expected run produces no heartbeat.
On how long an agent can run: METR's research measures the length of tasks agents can complete, but that headline is at 50% reliability, and the 80% horizon is much shorter. So do not read those numbers as "agents can reliably work for X hours unattended." Reliability for overnight runs comes from the structure above (short checkable steps, bounded loops, gated side effects), not from betting that the model will stay on track for hours. See METR's write-up for the methodology and its caveats.
What I'd do
If I were starting an unattended agent today, in this order:
- Write the failure policy first. For each step, decide retry, skip, fall back, or halt before writing any prompt.
- Build the validators before the agent. If you cannot check an output in code, you are not ready to run it overnight.
- Spend your effort on tools, not prompts. Rename, consolidate, tighten schemas, improve error messages, and make wrong calls impossible.
- Put budgets on everything, then add tracing per run, then add alerts for missing runs.
- Start as a workflow and promote to an agent only the steps that need open-ended exploration.
- Ship with a dry-run mode and watch a week of traces before you stop watching.
The pattern behind all of this: the model is the least deterministic component, so surround it with the most deterministic engineering you can. That is what makes an agent boring enough to trust.
If you are putting an agent into production and want a second pair of eyes on the architecture, you can reach me at lazar-milicevic.com/#contact, or keep reading more field notes on the blog.
Frequently asked questions
How do you make an AI agent run reliably without human supervision?
The gap between a demo agent and an overnight agent is almost entirely engineering around the model, not the model itself. I decompose the task until every step produces a concrete artifact, validate it in code, and decide at design time what happens on failure. Every step needs a bounded retry count and an explicit exhaustion policy such as skip, halt, or fallback. Without those, an agent can retry the same malformed request hundreds of times and burn budget while producing nothing.
What is the difference between an AI workflow and an AI agent?
If the steps are known in advance, it is a workflow: a fixed sequence of LLM calls with code between them, which is cheaper, faster, and easier to debug. A real agent loop makes sense only where the path depends on what the model discovers along the way, such as research where the number of searches is unknowable up front. In my content system, researching a topic is an agent, while publishing a finished article to the CMS is plain code with a validation step. Confusing the two is the most common architecture mistake I see.
How should I design tools for an LLM agent?
Treat tool design like an API for a very literal colleague and spend more time on it than on prompt tuning. Write long descriptions covering what the tool does, when to use it, when not to, and what each parameter means. Consolidate near-duplicate tools, namespace names, and return only the high-signal fields the next step needs. Make errors instructive, so a message like 'slug must be lowercase kebab-case' leads to a fix instead of a repeat of the same bad call.
How do you validate each step of an autonomous AI agent?
A step is ready to run unattended when its output can be verified without asking a human. For each step I ask three questions: what concrete artifact does it produce, how is that validated in code, and what happens if it fails. Validation should be deterministic where possible, such as schema checks, length bounds, or link resolution. If the only validator is another LLM, I treat that step as higher risk and add a second line of defense.
Can you force Claude to call a tool with a valid schema?
Setting strict: true on tool definitions enables schema validation of tool inputs. Combined with tool_choice set to any, it guarantees both that a tool is called and that the inputs follow the schema. One gotcha is that forced tool choice prefills the assistant message, so the model emits no natural-language explanation before the call. I use forced calls for extraction-style steps and leave auto for steps where reasoning out loud helps, and I check the live docs for which models support it.
Building something hard with AI or automation? I am open to talk.
Get in touch