How to build AI agents that survive failures, resume interrupted tasks, handle approvals, and execute reliably in production.
1. Why Do AI Agents Need Durable Execution?
Most AI applications start with a relatively straightforward execution model.
A user sends a message, the application invokes an LLM, the model may call a tool, and the application returns a response.
For simple interactions, this is usually sufficient.
However, agentic applications introduce a different level of complexity.
An agent may execute several model calls, invoke multiple tools, delegate tasks to other agents, wait for external systems, and require human approval before continuing.
Consider an agent responsible for reviewing and processing a business request:
- The user submits a request.
- The agent analyzes the request.
- It retrieves information from internal systems.
- It delegates an analysis task to a specialist agent.
- It generates a proposed action.
- It requests approval from an authorized user.
- After approval, it executes the operation.
- It records the result and completes the task.
This workflow may take seconds, minutes, or even several hours.
Now imagine the application crashes after step five.
What happens to the work already completed?
Does the agent start over? Are previous model calls executed again? Will the user need to submit the request again? Could an external operation be executed twice?
These are not unusual edge cases. They are fundamental engineering problems in long-running AI systems.
A conventional in-memory agent generally keeps its execution state inside the current process. When that process terminates unexpectedly, the state may be lost.
Saving conversation messages to a database does not solve this problem by itself.
Durable Execution is the ability to preserve workflow progress and resume execution from a known point after failures, interruptions, or external events.
It allows us to build agents that are not dependent on one uninterrupted application process.
The objective is not merely to make an agent remember what happened. It is to make the entire execution reliable.
2. State Persistence vs. Durable Execution
These concepts are related, but they are not interchangeable.
State Persistence
State persistence means storing information so that it can be retrieved later.
For an AI application, this might include:
- Conversation history
- User preferences
- Current task status
- Agent configuration
- Intermediate results
- Retrieved documents
- Tool execution results
For example, we can store conversation messages in PostgreSQL and load them when a user returns to the application.
This provides conversation continuity.
However, storing messages does not automatically tell us where an interrupted workflow should resume.
Durable Execution
Durable execution goes further.
It records the execution progress and the results of completed steps so that the workflow can continue without repeating all previous work.
Consider this example:
Agent Workflow | v Retrieve Context | v LLM Analysis | v Execute Tool | v Generate Report | X Application Crashes Without durable execution, the application may need to restart the entire workflow.
With checkpointing:
Agent Workflow | v Retrieve Context ✓ Checkpoint | v LLM Analysis ✓ Checkpoint | v Execute Tool ✓ Checkpoint | v Generate Report X Failed | v Application Restarts | v Resume from last completed checkpoint | v Generate Report | v Complete The completed results can be reused while the failed operation is retried.
This distinction becomes particularly important when model calls are expensive or when tools perform operations against external systems.
What about Agent Memory?
Memory and durable execution are also different concepts.
| Concept | Responsibility |
|---|---|
| Conversation history | What the user and agent discussed |
| Short-term memory | Information needed during the current interaction |
| Long-term memory | Information retained across interactions |
| Checkpointing | Where execution stopped and what has completed |
| Durable execution | How the workflow resumes and continues reliably |
For example, an agent may remember a user's preferences through long-term memory while a durable workflow maintains the execution state of an ongoing approval process.
One does not replace the other.
Reference: LangGraph — Persistence
3. Core Concepts Behind Durable Execution
Before choosing a framework, it helps to understand the underlying engineering primitives.
3.1. Checkpointing
A checkpoint is a persisted representation of workflow state at a specific point in execution.
Depending on the runtime, it may include:
- Completed step identifiers
- Step outputs
- Workflow state
- Execution metadata
- Pending operations
- Current task status
- References to external payloads
When a workflow resumes, the runtime uses these checkpoints to determine which operations have already completed.
Checkpointing is particularly useful when a workflow contains expensive or long-running operations.
However, we should avoid storing unnecessarily large objects in every checkpoint.
For example, instead of persisting a 50 MB document inside workflow state, we can store it in object storage and keep only its identifier and metadata in the checkpoint.
3.2. Replay
Replay is the process of reconstructing workflow execution using its previously recorded history.
Some durable execution engines replay workflow code to rebuild the current execution state.
This introduces an important engineering requirement: deterministic execution.
If the same workflow is replayed with the same history, it should make the same workflow decisions.
Consider:
async function workflow() { const timestamp = Date.now(); // Continue execution... } If the workflow is replayed later, Date.now() returns a different value.
This could change its execution path.
Likewise, direct network calls, random values, and database queries should not be performed inside deterministic workflow definitions when the runtime expects replay-safe code.
These operations should be isolated into activities or durable steps.
For example:
async function workflow() { const timestamp = await getCurrentTimeStep(); // Continue using the recorded result... } The durable runtime records the step result and reuses it during replay.
This is a foundational concept in systems such as Temporal.
3.3. Durable Steps
A durable workflow is usually divided into independent execution units.
For example:
Workflow | +-- Step 1: Retrieve documents | +-- Step 2: Generate analysis | +-- Step 3: Validate output | +-- Step 4: Request approval | +-- Step 5: Execute operation | +-- Step 6: Save final result Each completed step produces a checkpoint.
If step five fails, the workflow should not need to repeat document retrieval, model analysis, and validation.
This also allows us to apply different retry policies to different operations.
3.4. Durable Waits
A workflow may need to wait for something that is outside the application's control.
Examples include:
- Human approval
- An external webhook
- A scheduled execution time
- A payment confirmation
- A response from another agent
- Completion of a long-running background operation
A regular asynchronous function can wait for an event, but keeping the process alive indefinitely is not a scalable approach.
Durable execution systems can suspend the workflow, persist its state, and resume it when the expected event arrives.
The application does not need to keep a worker occupied during the waiting period.
3.5. Retries and Recovery
Failures should be handled according to their type.
A network timeout, provider rate limit, validation error, and authorization failure should not necessarily follow the same retry policy.
A retry policy should define:
- Maximum attempts
- Initial delay
- Backoff strategy
- Maximum delay
- Retryable error categories
- Overall execution deadline
- Cancellation behavior
A durable execution engine provides the infrastructure for retries, but the application still needs to define what should be retried and why.
4. The Most Important Concept: Idempotency
This is one of the most important aspects of building reliable AI agents.
Durable execution can resume workflows and retry failed steps. But retries can introduce duplicate side effects.
Consider an agent that generates a report and publishes it to an external system.
The operation succeeds, but the application crashes before the workflow records the result.
When execution resumes, the workflow may attempt to publish the report again.
Without additional protection, we could create two reports.
This is why idempotency matters.
An operation is idempotent when executing it multiple times with the same logical input produces the same intended outcome as executing it once.
For example, consider a report publishing API:
async function publishReport(input: { reportId: string; content: string; operationId: string; }) { const response = await fetch( 'https://api.example.com/reports', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': input.operationId, }, body: JSON.stringify({ reportId: input.reportId, content: input.content, }), }, ); if (!response.ok) { throw new Error( `Report publishing failed: ${response.status}`, ); } return response.json(); } The important detail is operationId.
It must be generated and persisted by a trusted part of the application before the external operation is attempted. It should not be an arbitrary identifier generated by the LLM.
The receiving service must also implement idempotency-key handling. Merely sending the header does not guarantee duplicate prevention.
Why isn't exactly-once execution automatic?
This is a common misunderstanding.
A workflow engine can guarantee that a successfully checkpointed step is not unnecessarily executed again during replay.
However, an external side effect and the workflow's checkpoint cannot always participate in the same atomic transaction.
There is a failure window:
- An external API accepts the request.
- The external operation succeeds.
- The application crashes.
- The workflow has not recorded the successful result.
- The workflow retries.
This means we should not assume that every external operation executes exactly once.
A more accurate approach is to design for:
- At-least-once execution attempts.
- Idempotent external operations.
- Durable operation identifiers.
- Deduplication at the receiving service.
- Transactional outbox patterns where appropriate.
- Reconciliation for operations whose result is uncertain.
For critical business operations, this distinction is non-negotiable.
A durable workflow is not a substitute for transactional business design.
5. Human-in-the-Loop: When an Agent Needs Approval
Not every operation should be executed automatically.
In enterprise applications, an agent may need authorization before performing an operation that has financial, operational, or security implications.
For example:
- Publishing a report
- Updating customer information
- Executing a database modification
- Approving a business transaction
- Changing production configuration
- Triggering an external workflow
A traditional implementation may look like this:
Agent | v Generate Proposed Action | v Save Pending Approval | v Notify User | v Wait for Response | v User Approves | v Continue Agent The problem is that the application needs to preserve enough state to resume the original execution.
A durable workflow makes this much easier.
It can:
- Execute the agent until an approval boundary.
- Persist the current state.
- Suspend execution.
- Notify the appropriate user.
- Wait for an approval event.
- Resume from the saved state.
- Continue or abort based on the decision.
The approval process may take seconds, hours, or days.
Importantly, the application should verify the identity and authorization of the person responding. The decision should be bound to the original operation, its parameters, and its expiration policy.
An approval should not be a generic boolean that can be reused for another action.
6. Implementing Durable Agents with Vercel AI SDK
For TypeScript-based applications, the Vercel AI SDK ecosystem provides an interesting integration through WorkflowAgent.
The standard ToolLoopAgent runs its execution loop in memory.
This is suitable for short interactions, but its progress can be lost when the underlying process terminates.
WorkflowAgent, provided through @ai-sdk/workflow, runs the agent inside the Vercel Workflow runtime.
It provides:
- Persistent execution progress
- Independent tool retries
- Recovery after process interruptions
- Human approval that can survive long waits
- Workflow-level execution visibility
- Resumable chat streaming through
WorkflowChatTransport
An important distinction: durability comes from the Workflow runtime, not simply from changing the agent class.
Step 1: Install the packages
pnpm add ai @ai-sdk/workflow workflow zod Step 2: Define durable tool operations
A tool operation can be marked as a durable step using the 'use step' directive.
async function searchDocumentsStep(input: { query: string; }) { 'use step'; const response = await fetch( `https://api.example.com/search?q=${encodeURIComponent( input.query, )}`, ); if (!response.ok) { throw new Error( `Document search failed: ${response.status}`, ); } return response.json(); } The runtime can persist the result of this step and retry it independently when a retryable failure occurs.
This is particularly useful when the operation interacts with an unreliable external service.
Step 3: Create a WorkflowAgent
import { WorkflowAgent, type ModelCallStreamPart, } from '@ai-sdk/workflow'; import { convertToModelMessages, tool, type UIMessage, } from 'ai'; import { getWritable } from 'workflow'; import { z } from 'zod'; async function searchDocumentsStep(input: { query: string; }) { 'use step'; const response = await fetch( `https://api.example.com/search?q=${encodeURIComponent( input.query, )}`, ); if (!response.ok) { throw new Error('Document search failed'); } return response.json(); } async function publishReportStep(input: { reportId: string; }) { 'use step'; const response = await fetch( 'https://api.example.com/reports/publish', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(input), }, ); if (!response.ok) { throw new Error('Report publishing failed'); } return response.json(); } export async function runAssistant( messages: UIMessage[], ) { 'use workflow'; const agent = new WorkflowAgent({ model: 'anthropic/claude-sonnet-4-6', instructions: ` You are an enterprise research assistant. Search internal documentation when needed. Prepare reports based on verified information. Never publish a report without approval. `, tools: { searchDocuments: tool({ description: 'Search internal documentation.', inputSchema: z.object({ query: z.string(), }), execute: searchDocumentsStep, }), publishReport: tool({ description: 'Publish a prepared report.', inputSchema: z.object({ reportId: z.string(), }), needsApproval: true, execute: publishReportStep, }), }, }); const result = await agent.stream({ messages: convertToModelMessages(messages), writable: getWritable<ModelCallStreamPart>(), }); return { messages: result.messages, }; } This example demonstrates the core pattern:
- The workflow function is marked with
'use workflow'. - Tool implementations that need durable retries are marked with
'use step'. - The report publishing tool requires human approval.
- The workflow can suspend and resume while waiting for the approval decision.
For a real deployment, the publishing operation should also implement server-controlled idempotency and authorization. The simple example focuses on the workflow integration rather than the complete business security layer.
Step 4: Understand the difference between WorkflowAgent and ToolLoopAgent
| Feature | ToolLoopAgent | WorkflowAgent |
|---|---|---|
| Execution | In-memory | Durable workflow |
| Process restart recovery | No automatic recovery | Supported by runtime |
| Tool retries | Application-managed | Per-step retry support |
| Long approval waits | Requires custom handling | Durable suspension |
| Tool execution visibility | AI SDK telemetry | Workflow step visibility |
| Streaming | Direct stream consumption | Workflow writable |
The recommended approach is not to replace every ToolLoopAgent with WorkflowAgent.
For short, synchronous agent interactions, an in-memory agent is often simpler.
Use durable execution when the workflow needs to outlive its original request, survive process interruptions, wait for external events, or retry individual operations independently.
Step 5: Make Chat Streaming Resumable
One challenge in long-running AI applications is that the backend workflow and the frontend connection have different lifecycles.
A user might:
- Refresh the browser.
- Lose network connectivity.
- Navigate away from a page.
- Reconnect after a function timeout.
A durable workflow may still be running even though the original HTTP connection has disappeared.
WorkflowChatTransport provides a mechanism for resuming the chat stream using the workflow run ID and previously received stream chunks.
A simplified client configuration:
'use client'; import { useChat } from '@ai-sdk/react'; import { WorkflowChatTransport } from '@ai-sdk/workflow'; import { useMemo } from 'react'; export default function Chat() { const transport = useMemo( () => new WorkflowChatTransport({ api: '/api/chat', maxConsecutiveErrors: 5, initialStartIndex: -50, }), [], ); const { messages, sendMessage } = useChat({ transport, }); // Render conversation and handle user input. } The server also needs the corresponding workflow stream endpoints.
This is an important architectural detail: a durable backend does not automatically make the browser connection durable. The frontend needs a way to reconnect and retrieve the appropriate part of the workflow stream.
References: Vercel — WorkflowAgent · AI SDK — WorkflowAgent Migration
7. LangGraph: Checkpointing and Interrupts
LangGraph approaches durable agent execution through graph state and persistence.
Its persistence architecture provides two complementary mechanisms:
Checkpointers
Persist graph state for a specific thread.
They support:
- Conversation continuity
- Recovery from interruptions
- Human-in-the-loop workflows
- Time travel
- Fault tolerance
Stores
Persist application-defined information across different threads.
They are useful for:
- User preferences
- Long-term memory
- Shared knowledge
- Cross-conversation information
A simplified configuration:
import { MemorySaver, StateGraph, Annotation, } from '@langchain/langgraph'; const State = Annotation.Root({ messages: Annotation<string[]>({ reducer: (current, update) => [ ...current, ...update, ], default: () => [], }), }); const builder = new StateGraph(State); // Add nodes and edges... const checkpointer = new MemorySaver(); const graph = builder.compile({ checkpointer, }); const result = await graph.invoke( { messages: ['Analyze the requested document.'], }, { configurable: { thread_id: 'analysis-123', }, }, ); This example uses an in-memory checkpointer for illustration.
It is not suitable for surviving process restarts. For production, use a persistent implementation such as PostgreSQL or another supported durable checkpointer.
Human-in-the-loop with interrupts
LangGraph provides the interrupt() function to pause a graph and wait for external input.
Conceptually:
import { interrupt, Command, } from '@langchain/langgraph'; async function approvalNode(state: { proposedAction: string; }) { const decision = interrupt({ action: state.proposedAction, message: 'Approval is required.', }); if (!decision.approved) { return { status: 'rejected', }; } return { status: 'approved', }; } The graph persists its state through its checkpointer.
Later, the application can resume the same thread using Command with the user's response.
This is useful for workflows where execution must pause for approval or additional input.
One subtle but important implementation detail: when resuming an interrupted graph, the interrupted node may execute again from its beginning. Operations performed before interrupt() therefore need to be designed carefully to avoid duplicate side effects.
References: LangGraph Persistence · LangGraph Interrupts
8. Temporal: Durable Execution as an Infrastructure Primitive
Temporal takes a different approach.
Rather than focusing specifically on agent state or conversation management, Temporal provides a general-purpose durable execution platform.
It is designed for long-running, stateful workflows that need to survive failures.
Two concepts are particularly important:
Workflow
Defines the orchestration logic and execution history.
Activity
Performs external or non-deterministic operations such as API requests, database access, LLM calls, and file operations.
A simplified AI workflow could look like this:
Temporal Workflow | v Retrieve Context Activity | v LLM Analysis Activity | v Validation Activity | v Wait for Approval Signal | v Execute Business Operation | v Complete Temporal records workflow history and uses deterministic replay to reconstruct execution state.
External interactions belong in Activities so their results can be recorded and reused during replay.
This separation is fundamental.
An LLM invocation should not be performed directly in a deterministic workflow definition. It should be executed as an Activity.
The same applies to database calls and external HTTP requests.
When does Temporal make sense?
Temporal is particularly relevant when:
- Workflows span multiple services.
- Tasks may run for hours or days.
- Strong recovery requirements exist.
- Human approval is part of the process.
- Many independent workers execute tasks.
- Workflow execution is critical to business operations.
It also introduces additional infrastructure and operational responsibilities.
Teams need to understand workflow history, Activity retries, Worker deployment, workflow versioning, and task queue management.
References: Temporal — Durable AI · Temporal TypeScript SDK · Temporal Workflow Determinism
9. Inngest and DBOS: Other Approaches Worth Evaluating
Temporal and LangGraph are not the only options.
For TypeScript-based applications, two other projects are worth considering.
Inngest
Inngest provides durable functions and agent workflows.
Its execution model is built around steps.
For example:
import { inngest } from './client'; export const researchWorkflow = inngest.createFunction( { id: 'research-workflow', }, async ({ event, step }) => { const context = await step.run( 'retrieve-context', async () => { return retrieveContext(event.data.query); }, ); const analysis = await step.run( 'generate-analysis', async () => { return generateAnalysis(context); }, ); const report = await step.run( 'validate-report', async () => { return validateReport(analysis); }, ); return report; }, ); The runtime checkpoints completed steps and resumes from the failed step when execution recovers.
Inngest also provides step.waitForEvent() for human approvals and other external events.
Its durable agent approach goes further by treating each model call and tool call as a separately checkpointed step.
This is useful when we want to write ordinary TypeScript functions instead of defining an explicit graph.
DBOS
DBOS provides durable execution f...
