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.currentStep and 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:

  1. Plans — breaks goals into steps, persists the plan in state
  2. Executes — runs steps one at a time, hibernating between them
  3. Reacts — wakes on webhooks, emails, and schedules
  4. Recovers — resumes from the last checkpoint after any interruption
  5. Delegates — hands off work to sub-agents and Workflows
  6. Maintains — prunes old data, archives completed work, manages its own lifecycle
  7. 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.

Was this helpful?

YesNo

On this page

Long-running agents·续 - 图1Docs

{"@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


Skip to content

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.