Durable Execution for AI Agents: Checkpoints, Retries and Resume

TL;DR
How durable execution keeps AI agents alive through crashes, deploys and multi-day approvals, with a LangGraph crash test and type-checked code.
Durable execution means an agent run's progress is stored outside the process that runs it, so a crash, a deploy or a two-day wait for a human resumes the run instead of restarting it. Every stack covered here - LangGraph, Mastra, Pydantic AI on DBOS, Inngest and Temporal - re-runs only the step that was in flight when things broke. The real decisions are how small you make your steps, and who re-drives a stalled run: your own code (the LangGraph open-source library), the framework's server on boot (Mastra workflows, and Mastra durable agents if you opt in) or an engine (Temporal, Inngest, DBOS).
Last updated: October 17, 2026
If you want the framework verdict, with a comparison table and a Graph API crash test, read Durable AI Agent Frameworks Compared. This post sits underneath it. It explains how checkpoints, journals, retries and resume actually work, shows a crash test I ran with the LangGraph Functional API, and builds the same refund agent natively in Mastra, Pydantic AI on DBOS, Inngest and Temporal.
Why long-running agents need it#
An agent run is a long chain of LLM calls and tool calls, and some of those tool calls have real side effects: a refund, an email, a merged pull request. Small per-step failure rates compound across a long chain, which is the argument in the agent reliability cliff. Three failure cases come up again and again:
- A crash mid tool call. The process dies after the payments API was called but before the result was saved.
- A deploy during a run. Your platform kills the old container while a 40-step agent loop is on step 23.
- A human approval that arrives days later. You cannot hold a process open for a manager who answers on Monday.
Durable execution answers all three with three primitives:
- Checkpoints save progress somewhere that outlives the process.
- Retries re-attempt only the unit that failed, not the whole run.
- Resumability lets any worker pick the run up from saved state, including a worker running newer code.
Checkpoint vs journal: the two mechanisms#
There are two ways to store progress, and they explain most of the behaviour differences you will hit.
Checkpoint and re-execute. The framework saves state at step boundaries. After a crash it loads the last checkpoint and runs the in-flight step again from its top. LangGraph's Graph API works this way: a checkpointer saves "a snapshot of the graph state saved at each super-step", where a super-step is "a single tick of the graph where all nodes scheduled for that step execute (potentially in parallel)" (LangGraph checkpointers). Mastra workflows also resume at a step boundary, from a stored snapshot.
Journal and replay. The engine records the result of each completed step. After a crash it replays your handler from the start, hands back the recorded results instead of re-running those steps, and executes only the step that never finished. The LangGraph Functional API, Inngest, Temporal and DBOS all work this way. Inngest's docs put it plainly: it "resumes a run by replaying your handler and returning saved step results, so completed work does not run again" (Inngest durable workflows).
%% title: Two ways to make an agent run durable
flowchart TD
R[Agent run] --> A{Persistence model}
A -->|Checkpoint| C1[Save state at step boundary]
C1 --> C2[Crash]
C2 --> C3[Load last checkpoint]
C3 --> C4[Re-run in-flight step from its top]
A -->|Journal and replay| J1[Record each completed step result]
J1 --> J2[Crash]
J2 --> J3[Replay handler from the start]
J3 --> J4[Return recorded results, run only unfinished step]The two models share one consequence. If the process dies after a side effect happened but before its result was recorded, that side effect runs again. Neither model can know the payments API already accepted the request. That is why idempotency keys are not optional in any durable agent, and why replay engines add a second rule: code outside a step re-runs on every replay, so it has to be deterministic.
What a resume actually looks like (tested)#
To see the mechanism rather than describe it, I ran a refund agent on the LangGraph Functional API with a SQLite checkpointer, langgraph==1.2.16 and langgraph-checkpoint-sqlite==3.1.2 on Python 3.11. Each phase ran in a separate process, and no model calls were made. A ledger file counts how many times each piece of code runs. Condensed:
@task
def plan_refund(order_id: str) -> dict:
bump("plan_refund")
return {"order_id": order_id, "amount_cents": 4200}
@task
def issue_refund(plan: dict) -> str:
if bump("issue_refund") == 1:
os._exit(1) # simulate the process dying mid side effect
# idempotency key: stable ID + step name, passed to the payments API
return f"refund:{plan['order_id']}:issue_refund"
conn = sqlite3.connect("checkpoints.sqlite", check_same_thread=False)
@entrypoint(checkpointer=SqliteSaver(conn))
def refund_agent(order_id: str) -> dict:
bump("entrypoint_body")
plan = plan_refund(order_id).result()
refund_id = issue_refund(plan).result()
approved = interrupt({"question": "Issue store credit too?", "refund_id": refund_id})
return {"refund_id": refund_id, "approved": approved}
config = {"configurable": {"thread_id": "order-42"}}
# process 1: refund_agent.invoke("order-42", config, durability="sync")
# process 2: refund_agent.invoke(None, config, durability="sync")
# process 3: refund_agent.invoke(Command(resume=True), config, durability="sync")
The ledger after each phase:
start (crash) {'entrypoint_body': 1, 'plan_refund': 1, 'issue_refund': 1}
recover {'entrypoint_body': 2, 'plan_refund': 1, 'issue_refund': 2}
-> returned __interrupt__ with the approval question
approve {'entrypoint_body': 3, 'plan_refund': 1, 'issue_refund': 2}
-> {'refund_id': 'refund:order-42:issue_refund', 'approved': True}
Four things fall out of those numbers:
- The entrypoint body ran three times. Replay "starts at the beginning of the entrypoint while LangGraph restores completed task and subgraph results from the checkpointer instead of recomputing them" (Functional API docs). Anything outside a
@taskre-runs on every resume, including the approval resume. - The completed task ran once.
plan_refundwas memoized and never touched again. - The in-flight task ran twice. The docs warn that "a task that started but did not finish may run again on that resume, so design side effects to be idempotent." In production, the second
issue_refundmust send the same idempotency key so the payments provider drops the duplicate. - Nothing re-drove the run by itself. The new process had to call
invoke(None, config), which is how the docs say to resume after an error. With the open-source library, deciding when to do that is your job.
The Interrupt object printed in this version also carries a response_schema field (None by default), which arrived in langgraph 1.2.12 on September 21, 2026. The October 9 comparison ran the Graph API version of this test, where the interrupted node restarted from its top. In the Functional API each @task is its own memoized unit, so what you put inside tasks decides exactly what can repeat.
Two settings matter here. durability="sync" persists before the next step starts. The "async" mode carries "a small risk that LangGraph does not write checkpoints if the process crashes", and "exit" means "you cannot recover from system failures (like process crashes) mid-execution" (checkpointers). And InMemorySaver loses everything on restart, so production means PostgresSaver (the one "used in LangSmith", per the checkpointers page), SQLite, MongoDB or Cosmos DB. On Postgres, keep the thread_id under 255 characters (persistence).
The replay engines below (Inngest, Temporal, DBOS) follow the same sequence when a worker dies mid step, except that the engine, not your code, starts the retry:
%% title: Crash and resume with memoized steps
sequenceDiagram
participant E as Engine
participant W as Worker
participant DB as State store
E->>W: run handler
W->>DB: save plan-refund result
W->>W: issue-refund starts
Note over W: process dies
E->>W: retry on a new worker
W->>DB: read saved results
DB-->>W: plan-refund result
W->>W: issue-refund runs again
W->>DB: save issue-refund resultThe same refund agent in four more stacks#
Each example below does the same job: issue a refund with an idempotency key, then wait for a human to approve store credit. The TypeScript examples were type-checked with tsc --noEmit (strict, moduleResolution: bundler, TypeScript 7.0.2). The Python example was compiled and import-checked. No API keys were used and no model calls were made. payments() is a stand-in for any payments client that accepts an idempotency key.
Mastra workflows (TypeScript)#
Mastra's workflow engine is strong on typing. suspendSchema types what you send when pausing, resumeSchema types what the human sends back, and the snapshot lives in the storage provider you already configured, which "persist[s] across deployments and application restarts" (suspend and resume).
import { createStep, createWorkflow } from "@mastra/core/workflows";
import { z } from "zod";
import { payments } from "./payments";
const issueRefund = createStep({
id: "issue-refund",
inputSchema: z.object({ orderId: z.string(), amountCents: z.number() }),
outputSchema: z.object({ orderId: z.string(), refundId: z.string() }),
retries: 3, // overrides the workflow-level retryConfig for this step
execute: async ({ inputData, runId }) => {
const refundId = await payments({
...inputData,
idempotencyKey: `${runId}:issue-refund`,
});
return { orderId: inputData.orderId, refundId };
},
});
const approveCredit = createStep({
id: "approve-credit",
inputSchema: z.object({ orderId: z.string(), refundId: z.string() }),
outputSchema: z.object({ refundId: z.string(), credit: z.boolean() }),
suspendSchema: z.object({ reason: z.string() }),
resumeSchema: z.object({ approved: z.boolean() }),
execute: async ({ inputData, resumeData, suspend }) => {
if (resumeData === undefined) {
return await suspend({ reason: `Store credit for ${inputData.orderId}?` });
}
return { refundId: inputData.refundId, credit: resumeData.approved };
},
});
export const refundWorkflow = createWorkflow({
id: "refund-agent",
inputSchema: z.object({ orderId: z.string(), amountCents: z.number() }),
outputSchema: z.object({ refundId: z.string(), credit: z.boolean() }),
retryConfig: { attempts: 2, delay: 2000 },
})
.then(issueRefund)
.then(approveCredit)
.commit();
// Later, possibly on another process after a deploy:
export async function approve(runId: string) {
const run = await refundWorkflow.createRun({ runId });
return run.resume({ step: approveCredit, resumeData: { approved: true } });
}
Type-checked against @mastra/core@1.78.0 and zod@4.6.7. Retries can be set workflow-wide with retryConfig: { attempts, delay } or per step with retries, which wins (error handling). For cold recovery, workflow.getWorkflowRunById() plus createWorkflowStateReader() shows which steps are suspended and what payload they are waiting for, without reading the raw snapshot. A suspend() sets status suspended, while sleep() and sleepUntil() set waiting.
Mastra also re-drives stalled workflow runs for you. The workflows overview documents run.restart(), which resumes an active run "from the last active step", and workflow.restartAllActiveWorkflowRuns() for every active run of a workflow, with concurrency capped by recovery.workflowConcurrency (default 5). It also says that "when running the local mastra server, all active workflow runs will be restarted automatically when the server starts", through the boot-time Mastra.restartAllActiveWorkflowRuns(). A workflow can opt out of that boot-time sweep with autoRestartActiveRuns: false (Workflow reference).
That restart is also why the idempotency key matters here. A workflow step resumes from its top, so the issue-refund call above can repeat after any crash, whether the run is restarted on boot or by hand. The key is what makes the repeat safe.
For open-ended agent loops rather than fixed pipelines, Mastra has durable agents, still labelled beta (durable agents). They come in three factories: createDurableAgent() in process, createEventedAgent() in the background over PubSub, and createInngestAgent() from @mastra/inngest, where "each tool call becomes a memoized step that Inngest can retry independently". Recovery from a snapshot "can reissue LLM calls (with real cost) and re-execute tool calls." Unlike plain workflows, durable agent workflows are excluded from the blanket boot-time restart, because their recovery runs through the opt-in recovery.durableAgents: 'auto' path (workflows overview).
One correction to our October 9 post, which said multiple replicas would race to recover the same runs. Mastra has since documented run ownership. Every stream(), generate(), resume() and recover() call claims the run and "renews the claim every 10 seconds", and "a claim lapses 30 seconds after its last renewal". Once another execution claims the run, "writes from the execution that lost it are rejected" on stores that support run fencing, including @mastra/pg, @mastra/libsql, @mastra/mongodb on a replica set, @mastra/dynamodb and @mastra/redis. Every replica can now start with recovery.durableAgents: 'auto': "Replicas that boot together race to recover the same orphaned runs, but only one claim succeeds per run and the other replicas skip it." Without a fencing store or a shared PubSub lease, the docs still say to gate recovery behind leader election.
Pydantic AI on DBOS (Python)#
Pydantic AI does not ship its own engine. It lists "nine durable execution solutions, plus a builder for any other engine": Temporal, DBOS, Prefect, Restate and AWS Lambda co-maintained with those vendors, plus external integrations for Kitaru, Apache Airflow, Absurd and pgtask (durable execution overview). DBOS is the lightest of them: it runs in process as a library and checkpoints into Postgres or SQLite, so you get replay semantics without operating a new server.
from dbos import DBOS, DBOSConfig
from pydantic_ai import Agent
from pydantic_ai.durable_exec.dbos import DBOSDurability
dbos_config: DBOSConfig = {
"name": "refund_service",
"system_database_url": "sqlite:///dbos.sqlite", # Postgres in production
}
DBOS(config=dbos_config)
agent = Agent(
"openai:gpt-6.1-sol",
name="refund_agent", # stable: DBOS uses it to identify the workflows
instructions="Decide whether to refund the order, then call issue_refund once.",
capabilities=[DBOSDurability()], # model requests run as DBOS steps
)
@agent.tool_plain
@DBOS.step() # function tools are not wrapped automatically
def issue_refund(order_id: str, amount_cents: int) -> str:
key = f"{DBOS.workflow_id}:issue_refund" # idempotency key for the payments API
return f"refunded {amount_cents} on {order_id} (key {key})"
@DBOS.workflow()
async def handle_refund(order_id: str) -> str:
result = await agent.run(f"Customer asks for a refund on {order_id}.")
return result.output
Compiled with py_compile and imported as a module on pydantic-ai-slim[dbos,openai]==2.57.0 with dbos==3.3.0, where the tool registered as issue_refund. You call DBOS.launch() at startup, after the agent is constructed, then await handle_refund().
The caveats are all in the DBOS integration docs:
- Model requests and MCP calls become DBOS steps, but function tools are "not automatically wrapped". Add
@DBOS.stepto any tool that does I/O, as above. - Model, MCP and event-handler steps have no retries unless you pass a
StepConfig. The docs also recommend turning off the provider client's own retries, for examplemax_retries=0, so retries do not multiply. - DBOS has "no selective non-retryable-exception support", so with step retries on, a misconfigured agent burns its full retry budget before failing.
- Each agent needs a unique, stable
name. Keep step inputs and outputs small, under about 2 MB.
Inngest (TypeScript, Python, Go)#
Inngest is an event-driven engine, available as a hosted cloud or self-hosted, that calls your app over HTTP. When a step succeeds, "Inngest stores its result as a checkpoint", each step has its own retry counter, and "a wait does not keep a worker running" (durable workflows). In TypeScript SDK v4, checkpointing is the default: steps run back to back in the same handler, falling back to one request per step only when a step retries or steps run in parallel.
import { Inngest } from "inngest";
import { payments } from "./payments";
const inngest = new Inngest({ id: "support-app" });
export const refundAgent = inngest.createFunction(
{
id: "refund-agent",
triggers: [{ event: "support/refund.requested" }],
retries: 4, // each step gets its own counter
},
async ({ event, step, runId }) => {
// Code out here re-runs on every replay: keep it cheap and deterministic.
const plan = await step.run("plan-refund", async () => {
return { orderId: String(event.data.orderId), amountCents: 4200 };
});
const refundId = await step.run("issue-refund", async () => {
return payments({ ...plan, idempotencyKey: `${runId}:issue-refund` });
});
const approval = await step.waitForEvent("wait-for-approval", {
event: "support/credit.approved",
timeout: "7d",
match: "data.orderId",
});
return { refundId, credit: approval !== null };
},
);
Type-checked against inngest@4.24.0. By default "Inngest allows four retries after the first attempt", and you can throw NonRetriableError for permanent failures or RetryAfterError to control timing (error handling). step.waitForEvent() returns null on timeout, and match and if cannot be combined (waitForEvent reference). For the LLM call itself, step.ai.infer() offloads the inference request to Inngest's infrastructure, so on serverless "you don't pay for function execution while waiting for the provider's response", and step.ai.wrap() turns an existing SDK call into a step (AI orchestration).
The caveat: "Code outside a step can run again each time the handler replays", and the SDK uses a hash of the step ID as the state key. Rename "issue-refund" while runs are in flight and memoization no longer finds the old result. Read Inngest's versioning guide before changing a live function.
Temporal (TypeScript shown; Python, Go, Java and .NET exist)#
Temporal is built entirely around replay. A Workflow Execution runs "effectively once and to completion", whether your code executes for seconds or years, and a Replay "is the method by which a Workflow Execution resumes making progress", checking the commands your code generates against the existing Event History (workflow execution). LLM calls and tool side effects go in Activities, and approvals arrive as Signals.
// temporal-activities.ts: plain async functions, retried by Temporal
import { payments } from "./payments";
export async function planRefund(orderId: string) {
return { orderId, amountCents: 4200 };
}
export async function issueRefund(plan: { orderId: string; amountCents: number }, idempotencyKey: string) {
return payments({ ...plan, idempotencyKey });
}
// temporal-workflow.ts: deterministic, replayed from Event History
import { condition, defineSignal, proxyActivities, setHandler, workflowInfo } from "@temporalio/workflow";
import type * as activities from "./temporal-activities";
const { planRefund, issueRefund } = proxyActivities<typeof activities>({
startToCloseTimeout: "1 minute",
retry: { initialInterval: "1s", backoffCoefficient: 2, maximumAttempts: 5 },
});
export const approveCredit = defineSignal<[boolean]>("approveCredit");
export async function refundAgent(orderId: string) {
let approved: boolean | undefined;
setHandler(approveCredit, (decision) => {
approved = decision;
});
const plan = await planRefund(orderId);
const refundId = await issueRefund(plan, `${workflowInfo().workflowId}:issue-refund`);
// Durable timer: no worker is held while this waits.
const answered = await condition(() => approved !== undefined, "7 days");
return { refundId, credit: answered && approved === true };
}
Type-checked against @temporalio/workflow@1.24.0. A signal call returns as soon as the server accepts it, without waiting for the workflow to process it (message passing).
Three caveats matter for agents:
- Retries are unlimited by default. The default Activity policy starts at 1 second, doubles, caps at 100 seconds, and "the maximum attempt of retries are set to zero which is evaluated as unlimited" (retry policies). Set
maximumAttemptson anything that costs money, like a paid LLM call. Workflow Executions themselves do not retry by default. - History has a ceiling. Event History is limited to 51,200 events or 50 MB, with a warning at 10,240 events or 10 MB (limits). A long agent loop with many tool calls should Continue-As-New before it gets close.
- Workflow code must stay deterministic across deploys. "Adding, removing, or reordering await calls on Command-producing APIs such as Activities and timers" causes non-determinism errors on replay. Wrap changes in
patched('change-id')and calldeprecatePatch()once old executions finish (versioning).
Pydantic AI and the OpenAI Agents SDK both run on Temporal too. The comparison post covers those integrations, so I will not repeat them here.
Waiting for a human without holding a process#
All five stacks handle approvals the same basic way: persist state, pause, and resume when the answer arrives. None of them sleeps in memory.
- LangGraph:
interrupt(), theninvoke(Command(resume=...), config). - Mastra:
suspend()in a step, thenrun.resume({ step, resumeData }). - Inngest:
step.waitForEvent()with an explicit timeout. - Temporal: a Signal handler plus
condition()with a timeout. - Pydantic AI: deferred tools, covered in the comparison post.
%% title: Waiting days for approval without holding compute
sequenceDiagram
participant A as Agent run
participant S as Durable store
participant H as Human
A->>S: persist state and pause
Note over A: no process held open
H->>S: approve after 2 days
S->>A: resume with decision
A->>A: continue from paused stepThe trap is that your code can change between the pause and the answer. A run that paused under Friday's deploy resumes under Monday's. Store a version marker with the paused state, keep step IDs stable, and on Temporal gate changes with patched(). The chat UI side of this, where the approval button lives, is covered in durable approvals with the Vercel Chat SDK.
Production checklist#
- One side effect per step. A retry repeats the whole step, so a step that refunds and then emails will email twice when the email fails. Split them into separate LangGraph tasks, Mastra steps, Inngest steps, Temporal activities or DBOS steps.
- Derive an idempotency key from a stable ID plus the step name. Use the run ID, workflow ID or thread ID, as every example above does, and pass the key to the downstream API. Stripe's idempotent requests are the model to copy: the same key returns the same result instead of charging twice.
- Keep non-deterministic work out of replayed code. Time, random numbers, LLM calls and I/O belong inside a step. LangGraph's Functional API docs say this directly, and Temporal's docs say LLM invocations "should be placed in Activities" (retry policies).
- Retry in exactly one layer. Temporal's default is unlimited, Inngest's is four retries per step, DBOS steps have none unless configured, and Pydantic AI recommends
max_retries=0on the provider client. Stack all of these and one flaky provider can turn into dozens of paid calls. - Pick real durability. In LangGraph,
durability="sync"is the only mode that persists before the next step starts. - Watch payload size. Temporal caps history at 50 MB, and Pydantic AI suggests keeping DBOS step inputs and outputs under about 2 MB. Store large documents elsewhere and pass references. LangGraph task outputs must also be JSON-serializable.
- Keep names stable. Step IDs, agent names and activity names are state keys. Version real changes with Temporal
patched()or Inngest's versioning guidance. - Decide who re-drives orphaned runs. With LangGraph's open-source library that is your sweeper calling
invoke(None, config). Mastra workflows are restarted from their last active step when the Mastra server boots, or on demand withrun.restart()andrestartAllActiveWorkflowRuns(); opt a workflow out withautoRestartActiveRuns: falseif its side effects must never be re-driven by a blanket restart. In Mastra durable agents it isrecovery.durableAgents: 'auto'or your ownrecoverActiveRuns()call. With Temporal, Inngest and DBOS it is the engine. The supervision around that, including cost caps and receipts, is covered in long-running agents need harnesses.
Which layer should you pick#
If you do not want to operate another service, the database you already run is enough: a LangGraph checkpointer or Pydantic AI on DBOS in Python, or Mastra workflows on your existing storage in TypeScript. DBOS needs only Postgres or SQLite. If you are willing to run an engine, Temporal is the pick when replay, versioning and very long histories matter most, and Inngest when you want events, serverless deploys and per-step retries, which you can use hosted or self-host.
%% title: Picking a durability layer
flowchart TD
Q1{Willing to run an engine?} -->|No| Q2{Language}
Q2 -->|Python| P1[LangGraph checkpointer or Pydantic AI on DBOS]
Q2 -->|TypeScript| P2[Mastra workflows on your storage]
Q1 -->|Yes| Q3{What matters most}
Q3 -->|Replay, versioning, long histories| P3[Temporal]
Q3 -->|Events, serverless, per-step retries| P4[Inngest]Teams already on Vercel have one more option, Workflow and WorkflowAgent, explained in Vercel's durable execution programming model. For the full framework verdict, see the comparison post.
FAQ#
What is durable execution for AI agents?#
It is a way of running an agent so its progress is saved outside the process after each step. If the process crashes, gets redeployed or waits days for a human, the run continues from the last saved step instead of starting over. The two main ways to get there are checkpoint-and-re-execute (LangGraph Graph API, Mastra workflows) and journal-and-replay (LangGraph Functional API, Temporal, Inngest, DBOS).
Does a durable agent ever run a tool call twice?#
Yes, the one that was in flight when the crash happened. In my LangGraph Functional API test, the completed plan_refund task ran once while the crashed issue_refund task ran twice. Every engine here behaves the same way for an unfinished step, so pass an idempotency key to anything with side effects.
Do I need Temporal or Inngest to make an agent durable?#
No. LangGraph checkpointers, Mastra workflows and Pydantic AI on DBOS all run on your own database, such as Postgres or SQLite, and Mastra's server restarts active workflow runs when it boots. An engine adds automatic re-driving of stalled runs that does not depend on your app server restarting, plus per-step retry policies and versioning tools, but it is another system to run or pay for.
How long can a durable agent wait for approval?#
As long as your storage keeps the state. Inngest's step.waitForEvent() and Temporal's condition() take explicit timeouts, such as "7d" or "7 days", and return a value you can branch on when time runs out. LangGraph interrupts and Mastra suspended steps wait until you resume them.
What breaks when I deploy new code mid-run?#
Replay determinism. If new code adds, removes or reorders steps, a replaying run no longer matches its recorded history. Use patched() on Temporal, keep Inngest step IDs stable, keep Pydantic AI agent names stable, and store a version marker with any paused run.
Sources#
- LangGraph checkpointers
- LangGraph persistence
- LangGraph Functional API
- LangGraph releases and 1.2.12
- Mastra workflows overview (restarting active workflow runs)
- Mastra Workflow reference
- Mastra suspend and resume
- Mastra workflow error handling
- Mastra durable agents
- Pydantic AI durable execution overview
- Pydantic AI durable execution with DBOS
- Inngest durable workflows
- Inngest error handling
- Inngest step.waitForEvent (v4)
- Inngest AI orchestration
- Inngest versioning
- Inngest self-hosting
- Temporal workflow execution
- Temporal workflow execution limits
- Temporal retry policies
- Temporal TypeScript versioning
- Temporal TypeScript message passing
- Stripe idempotent requests
- Package versions: langgraph on PyPI, pydantic-ai on PyPI, @mastra/core on npm, inngest on npm, @temporalio/workflow on npm
Continue Reading#
- Durable AI Agent Frameworks Compared: One Run, Four Stacks - the framework verdict, with a Graph API crash test
- Mastra for Durable TypeScript Agents: Where It Fits and Where It Does Not - where Mastra fits
- Long-Running Agents Need Harnesses, Not Hope - supervision around long runs
- LangChain and LangGraph in Late 2026: What Is New and Who Should Use It
- Pydantic AI V2 in Late 2026: Capabilities, Graphs, Evals and MCP
- Human-in-the-Loop Patterns for AI Agents: Approvals in 5 SDKs - the approval decisions themselves: approve, edit, reject and remember in five SDKs
Get the next deep dive like this in your inbox
One email a week on AI Agents and the rest of the AI dev stack. Free.
Read next on AI coding tools
Durable AI Agent Frameworks Compared: One Run, Four Stacks
One agent run crashes mid tool call, waits two days for approval, then retries a charge. How LangGraph, Mastra, Pydantic AI and OpenAI Agents SDK handle it.
14 min readMastra for Durable TypeScript Agents: Where It Fits and Where It Does Not
Mastra fits TypeScript products whose agents need workflows, memory, tools, MCP, evals, and traces in one backend layer. Where it fits and where it does not.
8 min readVercel's New Durable Execution Programming Model: A Developer's Guide
Durable execution lands on Vercel. What it means for agents, long-running flows, and indie dev stacks - with code, gotchas, and where it fits the agent stack.
10 min readNew here? Start with
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.






