Long-running agents·续
On recovery, use the stash to construct a focused prompt rather than replaying everything.
Use the plan as context. If the agent has a structured plan, the plan itself provides sufficient context: "I am on step 3 of 7, the step was 'run CI pipeline', the result just arrived." This is the most robust approach for long-running agents — the plan is both a recovery mechanism and a context reconstruction strategy. Refer to the next section.
Planning as a durability strategy
A structured plan is not just useful for showing progress to users — it is a durability mechanism. An agent with a plan can recover from any interruption by looking at where it left off.
type Plan = {
goal: string;
steps: PlanStep[];
currentStep: number;
createdAt: string;
updatedAt: string;
};
type PlanStep = {
id: string;
description: string;
status: "pending" | "in_progress" | "complete" | "failed" | "skipped";
result?: unknown;
};
export class ProjectManager extends Agent<Env, ProjectState> {
async createPlan(goal: string) {
const steps = await this.keepAliveWhile(async () => {
return this.callLLM(`
Break down this project goal into concrete steps.
Return a JSON array of { id, description } objects.
Goal: ${goal}
`);
});
this.setState({
...this.state,
plan: {
goal,
steps: steps.map((s: { id: string; description: string }) => ({
...s,
status: "pending" as const,
})),
currentStep: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
});
await this.schedule(0, "executeNextStep");
}
async executeNextStep() {
const { plan } = this.state;
if (!plan || plan.currentStep >= plan.steps.length) {
this.setState({ ...this.state, status: "complete" });
return;
}
const step = plan.steps[plan.currentStep];
try {
const result = await this.keepAliveWhile(() => this.executeStep(step));
const updatedSteps = plan.steps.map((s) =>
s.id === step.id ? { ...s, status: "complete" as const, result } : s,
);
this.setState({
...this.state,
plan: {
...plan,
steps: updatedSteps,
currentStep: plan.currentStep + 1,
updatedAt: new Date().toISOString(),
},
});
await this.schedule(0, "executeNextStep");
} catch (error) {
const updatedSteps = plan.steps.map((s) =>
s.id === step.id ? { ...s, status: "failed" as const } : s,
);
this.setState({
...this.state,
plan: {
...plan,
steps: updatedSteps,
updatedAt: new Date().toISOString(),
},
});
}
}
}
This pattern has several advantages for long-running agents:
- Recovery is trivial — on restart, check
plan.currentStepand resume - Progress is visible — clients see which steps are done and what is next
- Re-planning is possible — if a step fails or requirements change, the agent can revise the remaining steps without losing completed work
- Human oversight — the plan is a natural approval checkpoint ("here is what I am going to do — proceed?")
- Context reconstruction — the plan tells the LLM where it is, what happened, and what to do next, without replaying the full conversation
Delegating to sub-agents
A project manager does not do everything itself. It delegates specialized work to sub-agents — each with their own identity, state, and lifecycle.
export class ProjectManager extends Agent<Env, ProjectState> {
async delegateTask(task: Task) {
const researcher = await this.subAgent(
ResearchAgent,
`research-${task.id}`,
);
const findings = await researcher.research(task.title);
this.updateTask(task.id, { status: "complete" });
return findings;
}
}
Sub-agents have their own state, schedules, durable fibers, and lifecycle. They are colocated under the parent, but each child stores its own SQLite data and runs callbacks with the child as this.
Because facets do not have independent alarm slots, the top-level parent owns the physical Durable Object alarm. The Agents SDK records which sub-agent owns each schedule or fiber recovery lease, wakes the parent, and routes the callback back into the child. The parent does not need to stay active while the sub-agent works — it can start the work, hibernate, and be woken by the child-owned schedule or recovery check.
For the full subAgent() API — typed RPC stubs, client routing, access control, storage isolation, and alarm-backed APIs — refer to Sub-agents. For AI-specific sub-agent streaming (running full LLM turns through a child agent), refer to Think: Sub-agent RPC.
Recovering interrupted LLM streams
The patterns above handle the project manager's coordination work — scheduling, delegating, polling. But the project manager also uses an LLM directly: generating plans, summarizing progress, drafting status emails. Those LLM calls stream tokens over a connection that cannot be resumed if the agent is evicted mid-response.
For chat-oriented agents built on AIChatAgent or Think, this is an even sharper problem — the user watches the response stream in real time and sees it stop mid-sentence. Durable recovery wraps every chat turn in a runFiber. This provides automatic keepAlive during streaming and a recovery hook when the agent restarts:
import { AIChatAgent } from "@cloudflare/ai-chat";
import type {
ChatRecoveryContext,
ChatRecoveryOptions,
} from "@cloudflare/ai-chat";
class ProjectChat extends AIChatAgent<Env> {
override async onChatRecovery(
ctx: ChatRecoveryContext,
): Promise<ChatRecoveryOptions> {
// ctx.partialText — text generated before eviction
// ctx.recoveryData — whatever you stashed via this.stash()
// ctx.messages — full conversation history
// ctx.createdAt — when the interrupted turn started
return {};
}
}
The right recovery strategy depends on the LLM provider:
| Provider | Strategy | How it works | Token cost |
|---|---|---|---|
| Workers AI | Continue from partial | continueLastTurn() — model continues via assistant prefill |
Low |
| OpenAI (Responses API) | Retrieve completed response | Stash responseId during streaming, retrieve on recovery |
Zero |
| Anthropic | Synthetic continuation | Persist partial, send a synthetic user message asking the model to continue | Medium |
| Other | Try prefill, fall back to synthetic | continueLastTurn() if the provider supports it, synthetic message otherwise |
Varies |
Use ctx.createdAt to suppress stale recoveries. For example, if a recovered chat turn is older than a few minutes, you may persist the partial answer but skip automatic continuation to avoid surprising the user with an old response.
AIChatAgent and Think always use durable recovery. The default path persists partial output and continues or retries the turn when safe. Override onChatRecovery when a provider has a better recovery strategy. Configure chatRecovery = { maxAttempts, terminalMessage, onExhausted } to tune the terminal experience.
If the agent is interrupted before any assistant stream chunks are written, there is no partial assistant message to continue. When the latest persisted message is still the unanswered user message from that turn, chat recovery retries the turn automatically unless onChatRecovery returns { continue: false }.
Managing state over time
An agent that runs for months accumulates data: conversation history, timeline events, completed tasks, schedule records. Without management, this grows unbounded.
Housekeeping
Schedule periodic cleanup to prune old data and archive completed work:
export class ProjectManager extends Agent<Env, ProjectState> {
async onStart() {
await this.schedule("0 0 * * *", "housekeeping", {}, { idempotent: true });
}
async housekeeping() {
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
const toArchive = this.state.tasks.filter(
(t) => t.status === "complete" && (t.completedAt ?? 0) < cutoff,
);
for (const task of toArchive) {
this
.sql`INSERT INTO archived_tasks (id, data) VALUES (${task.id}, ${JSON.stringify(task)})`;
}
this.setState({
...this.state,
tasks: this.state.tasks.filter(
(t) => !toArchive.some((a) => a.id === t.id),
),
});
this.deleteWorkflows({
status: ["complete", "errored"],
createdBefore: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
});
}
}
Conversation history management
For agents that use AIChatAgent, conversation history can grow large over extended lifespans. Without management, a 3-month conversation will exhaust the LLM's context window long before the project ends.
Strategies for managing conversation size:
- Sliding window — keep only the last N messages in the active context. Simple and predictable.
- Summarization — periodically summarize older messages and replace them with a compact summary. Original messages can remain in SQLite for audit.
- Selective retention — retain messages that contain decisions, approvals, and key context while pruning routine exchanges.
End of life
A long-running agent eventually completes its purpose. The project ships, the investigation concludes, the monitoring window closes. Clean up explicitly:
export class ProjectManager extends Agent<Env, ProjectState> {
async completeProject() {
const schedules = await this.listSchedules();
for (const schedule of schedules) {
await this.cancelSchedule(schedule.id);
}
this.setState({ ...this.state, status: "complete" });
// All SQLite data, schedules, and state are permanently deleted
await this.destroy();
}
}
this.destroy() is permanent. If you may need the agent's data later, archive it to an external store (R2, D1, or an API call) before destroying. For agents that might be reactivated, simply mark them as complete and let them hibernate — they cost nothing when idle.
When to use Workflows vs agent-internal patterns
Both Workflows and agent-internal primitives (schedules, fibers, queues) support long-running work. The right choice depends on the nature of the work:
| Agent-internal | Workflows | |
|---|---|---|
| Best for | Agent-centric work: scheduling, polling, state updates | Independent multi-step pipelines |
| Durability | SQLite (survives eviction) | Workflow engine (survives everything) |
| Retries | this.retry(), schedule-level retries |
Per-step retries with backoff |
| Max duration | Minutes per activation (with keepAlive) |
30 minutes per step, unlimited steps |
| Human approval | Build it yourself (state + WebSocket) | Built-in waitForApproval() |
| Complexity | Lower — everything is in the agent | Higher — separate class, wrangler config |
A pragmatic rule: if the work is about the agent managing its own lifecycle (checking deadlines, syncing state, sending reminders), use schedules and fibers. If the work is a discrete pipeline that could fail and retry independently (deploy, data processing, report generation), use a Workflow.
The project manager agent uses both: schedules for its own rhythms (daily standups, progress syncs), and Workflows for heavyweight operations (deployments, CI pipelines).
Summary
Long-running agents on Cloudflare are not long-running processes. They are durable entities that wake, work, and sleep — potentially over weeks or months. The key primitives:
| Primitive | Purpose |
|---|---|
setState() / this.sql |
Persist state across activations |
schedule() / scheduleEvery() |
Wake the agent at future times |
keepAlive() / keepAliveWhile() |
Prevent eviction during active work |
runFiber() / stash() |
Checkpoint and recover long tasks |
startFiber() |
Durably accept, inspect, and cancel jobs |
chatRecovery |
Recover interrupted LLM streams |
onRequest() / onEmail() / RPC |
Wake on external events |
runWorkflow() |
Delegate heavyweight multi-step work |
subAgent() |
Delegate specialized work to child agents |
| Structured plans in state | Enable recovery, visibility, and re-planning |
For the project manager agent, these compose into an agent that:
- Plans — breaks goals into steps, persists the plan in state
- Executes — runs steps one at a time, hibernating between them
- Reacts — wakes on webhooks, emails, and schedules
- Recovers — resumes from the last checkpoint after any interruption
- Delegates — hands off work to sub-agents and Workflows
- Maintains — prunes old data, archives completed work, manages its own lifecycle
- Ends — cleans up and destroys itself when the project is done
The agent does not need to run continuously to do any of this. It just needs to exist.
Related
- Durable Execution —
runFiber(),startFiber(),stash(), and crash recovery - Schedule tasks — delayed, cron, and interval tasks
- Retries — retry options and patterns
- Workflows — durable multi-step processing
- Store and sync state —
setState()and persistence - WebSockets — lifecycle hooks and hibernation
- Callable methods — RPC via
@callableand service bindings - Email routing — receiving inbound email
- Webhooks — receiving external events
- Human in the loop — approval flows
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/concepts/agentic-patterns/long-running-agents/#page","headline":"Long-running agents","description":"Build agents that persist for days, weeks, or months — surviving restarts, waking on demand, and managing work that spans far longer than any single request.","url":"https://developers.cloudflare.com/agents/concepts/agentic-patterns/long-running-agents/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/concepts/agentic-patterns/long-running-agents/og.png?v=276b7d8e629b5bd0","dateModified":"2026-08-20","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["AI"]}
description: Call large language models from within a stateful Cloudflare Agent with persistent context and autonomous scheduling. title: Calling LLMs image: https://developers.cloudflare.com/agents/concepts/calling-llms/og.png?v=04d8cc2178606c35
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.
Docs