Build Logs · 5 min read

Long-Running Agent Jobs

A pattern for agent work that outlasts a single request, with durable state, human control and spend limits.

AI agentsDurable ObjectsReliabilityDebugging

View as Markdown llms.txt For agents

Reference pattern: a message reaches a Worker, a classifier sends many-step work to a mission held in its own Durable Object, alarms run bounded slices on a strong model, tools act through an allow-listed browser, and progress and the final report return to the user

Most agent failures on long tasks are not model failures. They are structural. A tool loop that runs inside a single request has a fixed number of steps and a fixed amount of time, and work that needs more than that ends in the middle, usually with the model’s last sentence presented as the answer.

I saw this repeatedly with Agu, my personal agent. Asked to walk through a web application and report on it, he would take a few screenshots, announce the next screen, and stop. The model was capable. The shape of the work was wrong.

This post describes the pattern I now use for agent work that outlasts a request, and the design principles that came out of making it reliable. It is built on Cloudflare Workers and Durable Objects, but the principles apply to any agent runtime.

A request is not a job

Agu routes each message to a model with a small classifier: simple questions go to a cheaper model, work that needs tools goes to a stronger one. Routing works, but it decides which model answers, not how long it may keep working. Both routes ran the same tool loop, capped at five steps inside one request.

A larger model with the same cap produces the same result. Long work needs a different execution model, not a different model.

The pattern

A request starts a job instead of running it.

  • Routing. The classifier gained a third outcome, mission, for walkthroughs, research across several sources, and other many-step work. Those messages start a background job directly.
  • Durable state. Each mission lives in its own Durable Object, holding the goal, recorded findings, an action log, notes and a status.
  • Bounded slices. A Durable Object alarm wakes the mission for a slice of up to 14 tool steps on a stronger model, then schedules the next one. No request has to stay open.
  • Explicit completion. The model ends the job by calling a finish_mission tool with its report. It can also message the user mid-job, or pause and wait for a human decision.
  • Limits. A mission stops after 24 slices, one hour, or three consecutive errors, and reports what it has.

Design principles

Separate routing from duration

Choosing a model and granting time are different decisions. Keep the request path short and bounded, and move anything open-ended into a job with its own lifecycle.

Record state in code, not in the model’s own notes

The first version carried progress between slices as notes the model wrote about itself. When those notes were thin, the next slice repeated earlier work. The fix was to stop relying on self-reporting. The runtime now logs every tool call automatically, passes that log to the next slice with an instruction not to repeat it, and provides a record_finding tool that saves each result the moment it is produced. The final report is built from the full record, not from the last slice’s memory.

Make stop authoritative

A running slice loads the mission at the start and writes it back at the end. A stop that arrives in between is silently overwritten by the slice’s older copy, the classic read-modify-write race. Two changes make stop reliable: every write goes through one function that re-reads the stored status and never overwrites a stop, and a stop aborts the in-flight model call instead of waiting for it to finish.

Give humans a small, reliable control vocabulary

Stop and pause keep everything. Resume continues from the recorded findings, action log and notes. Starting a new mission archives the previous one rather than replacing it. When the agent needs a human, for a captcha, a code or a decision, it says so and waits.

Run independent work in parallel, in isolation

Independent parts of a job run as separate missions, each in its own Durable Object, with at most three at once. Registering missions has to be serialized: two start calls issued in the same model turn interleave, and without a lock one registration can be lost.

Budget every job, and the gateway beneath it

Each mission carries a spending cap, three US dollars by default, estimated from token usage. A trivial one-slice mission cost about nine cents by that estimate, so a long job with a growing context costs real money. A per-job cap is not enough on its own. The gateway that serves the agent should also carry a spend limit, so that no combination of jobs can exceed what the account allows. When that gateway limit is reached, every call fails with HTTP 429, which is the intended behavior and a reason to know the limit before testing anything that loops.

Operating notes

  • Confirm a fix reached the running instance. A Durable Object that never goes idle appeared to keep running the code it started with after a deploy. A mission that re-arms its own alarm never goes idle. I have not found this documented, so treat it as an observation: return a new field from a status route to confirm which code is running, and deploy when no job is active.
  • Isolate tests from real users. Background jobs message people. Run tests under a separate conversation identifier.
  • Let multi-step handoffs reconnect. A human takeover link that allows a single connection fails on a phone reload. Sessions that hand off several times should accept a reconnect.

Scope

  • The behavior of busy Durable Objects after a deploy is an observation from one incident, not documented platform behavior.
  • Cost figures are estimates from token usage. The gateway’s records are authoritative.

Closing

Long-running agent work is less about a better model and more about an honest execution model: bounded steps, state the runtime records itself, a stop that always wins, and money treated as a limit rather than an afterthought.

References

For agents

Summary: A pattern for long-running agent work on Cloudflare Durable Objects: requests start jobs instead of running them, alarms drive bounded slices, state is recorded in code, stop and pause are authoritative, independent jobs run in parallel, and spend is capped per job and per gateway.

Prerequisites

  • A Cloudflare Worker with a Durable Object per chat
  • A model that supports tool calls through an AI SDK loop
  • A small classifier or rule that routes many-step requests to a mission
  • Optionally a browser server for page-driving tools

Steps

  • Route many-step messages to a mission (a typed decision model with a mission class, or a tool the model can call)
  • Store the mission (goal, findings, automatic action log, notes, status) in Durable Object storage
  • Wake a slice with an alarm: up to 14 tool steps, with findings, the action log and notes in the prompt and an instruction not to repeat logged actions
  • Record each tool call automatically in code and provide record_finding, notify_tofa, wait_for_tofa and finish_mission tools
  • Funnel every mission write through one function that re-reads the stored status and never overwrites a stop; abort the in-flight model call on stop
  • Handle stop, pause and resume messages before any model call; archive a mission when a new one starts
  • Check that a fix reached the running Durable Object instance (return a new field) before trusting it; deploy when no slice is running

Diagrams, in words

  • A message reaches the Worker, which first handles stop, pause and resume. A classifier picks simple, tools or mission. Simple and tools take a quick reply path capped at 5 tool steps. A mission lives in a Durable Object (goal, findings, action log, notes, status) and is woken by an alarm for slices of up to 14 steps on Claude Sonnet, within limits of 24 slices, one hour and three errors. Slice tools are browser_task, fetch_url, record_finding, notify_tofa, wait_for_tofa and finish_mission; the browser tool talks to a headless Chrome server that only opens allow-listed hosts. Progress and the final report go back to Telegram.

Descriptions reflect the author's own deployments using generally available services and public documentation.

This post contains no credentials. Do not put tokens, API keys, service-token secrets or one-time codes into prompts or commits; use environment variables or secret stores.