Tacnode™
Back to Blog
AI Engineering

Durable Execution for AI Agents: Recovery Isn’t Coordination

What durable execution engines like Temporal, Restate, DBOS and Inngest guarantee for AI agents, and what they leave to you when many workflows share state.

Haitao Wang
Haitao Wang
Engineering
11 min read
Two workflow journals of completed steps run through a crash and resume intact, then both spend from one budget gauge outside the engine, which overflows past its limit in orange.

TL;DR: Durable execution records each step of a workflow so that after a crash it resumes where it left off instead of starting over. For AI agents, that means long runs survive restarts, model calls aren’t repeated, and a run can wait hours for a human. It does not make two workflows agree about state they both depend on, and a replayed step returns what it read back then, not what is true now. Coordination on shared state still has to happen where that state is written.

Agents broke the old assumption that a request finishes in a second. A single agent run can make twenty model calls, wait on a slow API, pause for a manager’s approval and resume the next morning. If the worker restarts halfway through, starting over means paying for every model call again and maybe repeating actions that already happened.

Durable execution engines address that problem. So teams adopt one, wrap each agent in a workflow, and watch crash recovery start working. Then a different class of bug appears: two workflows that each ran perfectly, each resumed correctly after a deploy, and together spent past a budget, booked the same slot or contacted the same customer twice.

That isn’t a failure of durable execution. It’s a guarantee it never made.

What Is Durable Execution?

Durable execution is a way of running code so that its progress survives failures. The engine records each completed step and its result. If the process crashes, the engine re-runs the function, skips the completed steps by returning their recorded results, and continues from the first unfinished step, as if the crash never happened.

The engines describe it in similar terms.

Temporal. The Temporal docs define a Replay as “the method by which a Workflow Execution resumes making progress,” and say “During a Replay the Commands that are generated are checked against an existing Event History.” Side-effecting work runs in Activities, which are retried on failure.

Restate. The Restate docs say it “tracks every step of your code execution in a journal,” recording “both the operation and its result,” and that on failure it “replays the journal, skipping completed steps and resuming from exactly where it left off.”

DBOS. DBOS is a library rather than a server. Its architecture docs say it “checkpoints those workflows and steps to a Postgres database,” and after a crash “uses those checkpoints to recover each of your workflows from the last completed step.”

Inngest. Inngest saves each step’s result in the run’s state store. In its durable workflows docs, “Later requests replay saved results (memoization),” so a step that already ran returns its saved output instead of running again.

The mechanisms differ (event history, journal, checkpoint table, memoized state) but the contract is the same: one workflow, run to completion, each finished step done once from the workflow’s point of view.

Why AI Agents Use It

The fit is obvious once you list what an agent run does.

Expensive steps. A model call costs money and seconds. Recording its output means a crash at turn twelve doesn’t re-pay for turns one through eleven, and the replayed run sees the same model outputs it saw the first time, so it doesn’t take a different path.

Long waits. Agents wait for humans, webhooks and timers. A durable workflow can sleep for a day and still resume if its worker restarts in between.

Retries. Tool calls fail. Engines can retry them with backoff and record the eventual success.

None of that is in question. The question is what the guarantee covers.

Determinism: The Rule Replay Imposes

Replay works only if the workflow code makes the same decisions the second time it runs. Temporal’s workflow definition docs state the requirement: “any time your Workflow code is executed it makes the same Workflow API calls in the same sequence, given the same input.” If a replayed command doesn’t match the recorded history, “the Workflow Execution returns a non-deterministic error.”

For agents, that settles where the model call goes. The same docs say to put “non-deterministic operations like API calls, LLM/AI invocations, database queries, and other external interactions” in Activities, which “execute outside the replay path.” Once a model call completes, its answer is recorded and replayed from then on, not regenerated.

Two consequences follow. Changing workflow code while executions are still open can break replay, so Temporal offers Worker Versioning and patching for code changes. And a replayed read has to return what it returned the first time, or the workflow might take a different path. Determinism is what makes the replay safe, and it is also why the replayed value is old.

Durable Execution in Agent Frameworks

Some agent frameworks now plug into these engines instead of building their own.

Pydantic AI. Its durable execution docs list Temporal, DBOS, Prefect, Restate and AWS Lambda among the supported engines, and say each “wraps every model request and tool call as its own durable unit.” Under the heading “Durability is not storage,” the same page says: “A durable engine keeps one run alive across crashes and restarts. It does not store your chat threads.”

OpenAI Agents SDK. Temporal’s integration announcement (July 30, 2025, generally available since March 23, 2026) says “every agent invocation is executed through a Temporal Activity,” with the agent’s orchestration running as a Workflow.

Either way, the guarantee covers one run. State that many runs share lives somewhere else, which is where the next section starts.

What the Guarantee Covers, and What It Doesn’t

Durable execution is a guarantee about one workflow’s progress. Three things sit outside it.

State outside the engine. A workflow that reads a budget from your database, decides and spends has the engine’s guarantee for its own steps. Another workflow doing the same thing at the same time has the same guarantee for its own steps. Neither guarantee says anything about the budget.

The freshness of a replayed read. A recorded step returns its recorded result on replay. That is the point: it makes the replay deterministic. But if step 2 read “budget remaining: 4,000” and the workflow then slept for six hours waiting on approval, step 3 runs against a value from six hours earlier. A crash isn’t required for this; the read is simply old by the time the workflow acts on it.

The edge between a side effect and its record. If a worker sends a payment and crashes before the engine records that the step finished, the step runs again. Temporal is explicit about this: its Activities docs recommend that Activity code be idempotent “so retries can be processed without duplicate side effects.” DBOS narrows the edge for writes to your own database: its datasources ensure “that each database operation inside a workflow runs exactly once even if the workflow is interrupted and retried,” by recording the outcome in the same transaction. For calls to an external API, that’s your job, usually with an idempotency key the downstream API honors.

The Coordination Tools Engines Do Have

It would be wrong to say durable execution engines know nothing about concurrency. Each offers a way to serialize work by key, and these are the right tool when the contended state maps to a single key.

If every write to a customer’s refund state goes through one Restate Virtual Object keyed by customer, or one Temporal workflow per customer, that state is serialized. The limits show up when the state doesn’t fit one key:

State that spans keys. A team budget drawn down by purchase workflows keyed by request. A global API quota consumed by every agent. You can route all of them through one key, but then every workflow that touches the budget queues behind the others, including their model calls.

State that other systems write. Engine-held state is only coordinated against writes that go through the engine. A budget that finance edits in an admin tool, or inventory that a storefront decrements, changes underneath the workflow.

Reads across many keys. A decision that needs a customer’s state, their household’s state and a velocity count across all merchants reads several keys at several moments.

EngineMechanismWhat it guarantees
TemporalWorkflow IDTemporal “guarantees that there can be at most one Workflow Execution with a given ID running at any point in time,” per the Workflow Id docs
RestateVirtual ObjectsThe service types docs list state as “Isolated per object key” and concurrency as “Single writer per key (+ concurrent readers)”
InngestConcurrency keysA limit on concurrently running steps per key; the concurrency guide says to “Choose a stable key that identifies the resource you want to protect,” and notes that “concurrency does not limit the number of functions in progress,” so a run that is sleeping doesn’t hold a slot
DBOSPartitioned queuespartition_concurrency sets the “Maximum workflows from any one partition running at once across all processes,” per the queues docs; a limit of 1 with a user ID as the partition key runs one workflow per user at a time

A Worked Failure: Two Approvals, One Budget

Here is an illustrative scenario. The numbers are constructed, not measured.

A procurement agent runs as a durable workflow, one per purchase request. Each workflow reads the team’s remaining budget, drafts the order, waits for a manager’s approval, then places the order. The team has 5,000 left this quarter.

text
09:00  Workflow A (request R-101, 3,000): step "read_budget" -> 5,000 remaining. Recorded.
09:02  Workflow B (request R-102, 2,500): step "read_budget" -> 5,000 remaining. Recorded.
09:03  Both workflows draft orders and sleep waiting for approval.
11:40  Worker restart. Both workflows resume; replay returns 5,000 for "read_budget".
13:15  Manager approves A. Workflow A places order: 3,000.
13:20  Manager approves B. Workflow B checks recorded budget (5,000 >= 2,500), places order: 2,500.
       Team has spent 5,500 against 5,000.

The replay was correct. The value it replayed was more than four hours old, and the other workflow had already spent against it.

Both workflows were durable. Both survived the restart without repeating a step. Both acted on a budget value recorded more than four hours before they spent it, and neither knew the other existed.

Keying both workflows by team with a single-writer primitive, such as one Temporal Workflow ID or one Restate Virtual Object per team, would serialize them, but then request R-102 couldn’t even be drafted while R-101 waited four hours for approval.

Two workflow lanes, A and B. Both read a budget of 5,000, survive a worker restart with that value intact, then spend 3,000 and 2,500, reaching 5,500 of 5,000

The Fix: Holds, and a Re-check at the Action

The pattern that works is to coordinate on the shared state itself, at the moment of writing, and to make the workflow’s steps calls into that coordination.

Reserve at request time. The first step places a hold against the budget with a conditional write. If the hold doesn’t fit, the request is rejected or queued before anyone approves it.

sql
BEGIN;
INSERT INTO budget_holds (hold_id, team_id, amount, status)
VALUES ($hold_id, $team_id, $amount, 'held')
ON CONFLICT (hold_id) DO NOTHING;
-- No row inserted: this step already ran. COMMIT and return the existing hold.

UPDATE budgets
SET    held = held + $amount
WHERE  team_id = $team_id
  AND  quarter_limit - spent - held >= $amount;
-- Zero rows updated: the hold doesn't fit. ROLLBACK and reject the request.
COMMIT;

The conditional UPDATE on the team’s budget row is what serializes concurrent holds. Under PostgreSQL’s default READ COMMITTED, a second update to the same row waits for the first to commit, then its WHERE clause “is re-evaluated to see if the updated version of the row still matches,” per the isolation docs. A hold_id derived from the workflow ID and step name makes a retried step a no-op.

Convert or release. On approval, a step converts the hold to spend in one transaction. On rejection or timeout, a step releases it. Releasing a hold is a compensation: it undoes the reservation, which is possible because no money has moved yet. See Rolling Back AI Agent Actions for what can and can’t be undone.

The budget is reserved at request time, so a later approval can’t overspend it.

Re-read after any wait. Anything a step read before a long sleep is a recorded fact about the past. Put the reads that the action depends on in the same step as the action, after the wait.

The engine still does its part: it makes sure the hold, the approval wait and the conversion each happen once, in order, across restarts. The shared store makes sure two workflows can’t both fit into the same budget.

A hold’s lifecycle: a request places a hold, which either converts to spend on approval or is released on rejection or timeout

Where Tacnode Fits

Tacnode is not a workflow engine. It doesn’t run steps, journal them, schedule timers or retry anything. Keep your engine for that.

What the engine doesn’t hold is the state many workflows share. The Tacnode Context Lake™ can hold it, as the decision context every workflow reads before it acts and writes after.

Holds, budgets and decision records. These can live in Tacnode as the system of record. The hold check and insert above runs as one ACID transaction, so two workflows reserving against the same budget are serialized at the write, without serializing the workflows themselves. Each decision written back is current context for the next workflow’s check. Multi-agent coordination covers the same pattern for agents outside workflows.

Context from your own systems. Orders, inventory and account data stay in the systems you run and arrive by change data capture. When a step needs to read them along with derived values, such as spend velocity across merchants, Tacnode serves the raw rows and the aggregates against the same set of ingested events, through one PostgreSQL-compatible interface. It doesn’t enforce anything inside a ledger it doesn’t hold.

If your workflows are partitioned, each owning its own customer’s state with no shared budgets or quotas, the engine’s per-key primitives are enough.

FAQ

AI AgentsDurable ExecutionWorkflowsConcurrencyShared State
Haitao Wang

Written by Haitao Wang

Nearly two decades at Microsoft, including the SQL Server engine. Leads engineering for the Tacnode Context Lake™.

Fresh Context Newsletter

The argument behind the posts — what breaks when AI acts on stale data, from the team building the Context Lake. About twice a month.

Subscribe on LinkedIn

Ready to see Tacnode Context Lake™ in action?

Book a demo and discover how Tacnode can power your AI-native applications.

Book a Demo