For engineers and engineering leaders who run LLM agents that take minutes to days: multi-step tool loops, human approvals, scheduled jobs. Eight modules on when a durable-execution engine is worth adopting, which one, and how to write agent code so that replay, retries and idempotency hold when a non-deterministic model sits in the loop. Built from the vendors’ own documentation and pricing pages as they read in October 2026.
Research current to 14 October 2026. Every price, limit and quotation was read from a live vendor page or the vendor’s documentation source on that date, except where the text says an archived copy or a third-party report was used.
Quotations are word for word from the named documentation page. Prices and limits carry the date they were read, because every vendor in this course changed its pricing page at least once in the twelve months before writing. Where a vendor’s site could not be reached, the text says whether the figure came from the vendor’s own documentation repository, from an archived copy with its capture date, or from a named third party. Callouts labelled My position are argument, not sourced fact. Comparisons that only a vendor makes about a competitor are labelled as the vendor’s claim. Every URL cited is listed under Primary sources at the end.
A classic request handler lives for milliseconds. An agent run lives for minutes, hours or days: it calls a model, reads the answer, runs a tool, calls the model again, waits for a person, sleeps until Monday, and repeats. The process hosting that loop will not survive the run. Deploys restart containers. Spot instances get reclaimed. Serverless functions have ceilings: Vercel’s function limits page, updated 24 August 2026, puts the default duration at 300 seconds, the Pro and Enterprise maximum at 800 seconds, and an extended maximum of 1,800 seconds in beta. The same page sends anyone who needs “unlimited execution time” to Vercel Workflows instead.
The vendors say the demand is real. Temporal’s funding announcement of 14 September 2026 put it plainly: “Customers are asking to run agents that work for days, weeks, or months, and this is becoming the new normal.” The same post reported “more than 4,300 paying customers, up 139% year over year” and that “In August alone, the platform processed 1.9 trillion billable actions, up more than 350% year over year.” Those are vendor-reported numbers, but the direction is not in dispute: long-running agent loops have become an infrastructure category.
So picture the concrete failure this course is about. A research agent has made 30 model calls and 30 tool calls over 40 minutes. The context it carries is now large. On step 31 the container dies. What happens next?
| Substrate | What happens after the crash | What gets redone | What gets double-done |
|---|---|---|---|
| Plain process (a loop in a script, a serverless function, a pod) | Nothing. The run is gone. Someone notices later, or nobody does. | Everything, by hand, if anyone restarts it | Any tool side effect from steps 1 to 30 that the restart repeats |
| Queue plus worker (a job message, a worker that picks it up, a retry policy) | The message becomes visible again and another worker starts the job from the beginning | All 30 model calls and all 30 tool calls | Every side effect from the first attempt, unless each tool was made idempotent by hand |
| Cron plus bookmark (a scheduled job that records how far it got in a database row) | The next scheduled tick reads the bookmark and resumes from the last recorded position | Only the work since the last bookmark write | Whatever happened between the last bookmark write and the crash |
| Durable engine (Temporal, Vercel Workflows, Inngest, Restate, DBOS) | The engine notices the worker is gone, re-runs the workflow code against its recorded history, and resumes at step 31 | Only step 31 | Only a side effect inside step 31 that completed before the result was recorded |
The third row is underrated and the fourth row is the product. A bookmark is a durable engine with one step boundary that you placed yourself. An engine is a bookmark after every step, placed by the framework, with the bookkeeping for retries, timers and waits done for you. That framing matters because it tells you what you are paying for: not magic, but a step ledger you did not have to write.
Each vendor describes the same mechanism in its own words, and the words are worth reading because they define what the engine guarantees.
Temporal: the Event History is “a complete, ordered log of everything that has already happened in a Workflow” and “the source of truth for everything that happens in the Workflow.” Vercel Workflows: “All inputs and outputs are recorded in an event log. If a deploy or crash happens, the system replays execution deterministically from where it stopped.” Inngest, on steps: each step “Will not be re-executed if it has already been successfully executed.” DBOS: “once a step completes and is checkpointed, it is never re-executed.” Restate: on replay, a handler that reaches a recorded action “will skip execution and will inject the response it finds in the journal.”
Put together, durable execution means three things. First, an event history: an append-only record of every step input and output, every timer and every external signal, stored outside the process. Second, deterministic replay: after a failure the engine re-drives the workflow code and feeds it the recorded results, so the code reaches the same point it was at without redoing the recorded work. Third, step boundaries: the code is divided into the orchestration that is replayed and the units of work whose results are recorded. Everything in this course follows from those three ideas.
There are two implementation families, and Inngest’s own documentation draws the line. It describes Temporal as using “a deterministic replay model where your entire workflow function is re-executed from the beginning on each step, relying on an internal event history to skip completed work,” and itself as using “a step-based memoization model where each step runs once, its result is persisted, and subsequent executions skip completed steps by injecting their stored results.” That is a vendor’s framing of a competitor, so read it as positioning. In practice both families re-run your function and substitute recorded results; the difference is in how strictly the orchestration code must behave between steps, which is module 2.
Draw a box around each step. Inside the box, the engine promises nothing except that it will try again: the code runs at least once, and if the process dies after the side effect but before the result is recorded, it will run again. Between boxes, the engine promises everything: results are recorded, the orchestration is replayable, and a crash costs you at most the step in flight.
For an agent this has one consequence that dominates the rest. The expensive thing in an agent loop is the model call. It costs money per token, it takes seconds, and it returns a different answer each time. If the model call is inside a step, a crash costs you one call. If it is outside a step, in the orchestration code, a crash costs you every call since the start, and worse, as module 2 shows, the replay may not even converge. Temporal’s documentation for its OpenAI Agents SDK integration states the design directly: model calls “retry durably and are not repeated during Workflow replay.” Inngest’s agent tool-loop guide shows the two versions side by side and labels the bare call: “Not durable — if this fails, you lose all prior work.”
Not every agent needs an engine. A daily-brief agent that wakes once, fetches sources, writes a summary and posts it is a cron job. The scheduled agents course on this site builds exactly that with a bookmark, a post-then-record rule and a budget cap, and it is right to. The run is short, it has one side effect, and the side effect can be made idempotent with a key. Adding an orchestration engine to that job adds a dependency and a programming model for no durability you did not already have.
What forces the move is a wait. The moment a run has to pause for a person, pause for a webhook, or sleep until next week while keeping its place, a cron plus bookmark becomes a hand-written state machine with a polling loop. Module 4 is about why engines make waits cheap and bookmarks make them painful. The second forcing function is fan-out: once one run spawns twenty sub-tasks whose results must be joined, bookkeeping the bookmark by hand stops being fun. The third is retry with compensation: when a failed step must undo three earlier side effects, you want the saga machinery from module 3 rather than a bespoke undo table.
A cron plus a bookmark is enough until you need a wait. Teams reach for a durable engine too early because the demo is impressive and too late because by then the hand-rolled state machine has a name and an owner. The test is mechanical: if any run of your agent must pause for longer than your process is allowed to live, or must survive its own deploy, you need an engine. If every run is a short, idempotent, post-then-record job, you need a bookmark and a budget. The strongest objection is that engines are cheap now, so why not start there. The answer is that the determinism contract in module 2 is a real tax on how your team writes code, and you should pay it when the wait appears, not before.
Two things, and they are the things people most often assume they are getting.
First, durability does not make the agent right. A workflow that replays perfectly will replay a wrong plan perfectly. Evaluation, routing and the evaluator-optimizer loop belong to the harness layer above the substrate; the dynamic workflows course covers those patterns. Nor does durability stop a prompt injection: a hijacked agent with a durable engine is a hijacked agent that cannot be killed by a restart. The containment controls in the securing agents course sit beside the engine, not inside it.
Second, durability does not make tool side effects exactly-once. The engine records a step’s result after the step returns. If the process dies between the external call and the recording, the step runs again. Every engine in this course says so in its own words, and module 3 is about what you do with that.
Plain process: nothing is re-executed unless a human restarts it, and then everything is, so every side effect from steps 1 to 30 can happen twice. Queue plus worker: the whole job re-runs from step 1 automatically; the same double-execution risk, now without a human in the loop to notice. Cron plus bookmark: work since the last bookmark write is re-executed; if the bookmark is written after each step, only step 31 is redone and only its side effect can double. Durable engine: the orchestration code is replayed against the history, steps 1 to 30 are served from the record, step 31 runs again, and only a side effect inside step 31 that completed before its result was recorded can double. Notice that the bookmark-per-step design and the engine give the same answer. The engine’s value is that it also gives that answer for timers, waits, fan-out and retries without you writing the ledger.
Replay works by re-running your orchestration code and handing it recorded results in order. That only works if the code asks for the same things in the same order every time it runs. Every engine states this rule, and the statements are strikingly similar.
Temporal’s Workflow Definition page: “all operations that do not purely mutate the Workflow Execution’s state should occur through a Temporal SDK API,” and in a tip aimed at this course’s readers: “To handle non-deterministic operations like API calls, LLM/AI invocations, database queries, and other external interactions, put them in Activities.” Vercel’s Workflow SDK documentation: workflow functions “Must be deterministic to allow resuming after failures,” because “the workflow code gets re-run multiple times during its lifecycle.” DBOS: “The workflow function must be deterministic,” with database access, third-party API calls, random numbers and the local time belonging “in a step instead of directly in the workflow function.” Inngest: “Any non-deterministic logic (such as DB calls or API calls) must be placed within a step.run() call.” Restate: “Always immediately await ctx.run, before doing any other context calls.”
What happens when you break the rule is also documented. Temporal: “If a generated Command doesn’t match what it needs to in the existing Event History, then the Workflow Execution returns a non-deterministic error.” Vercel’s error table lists REPLAY_DIVERGENCE: “A replay could not consume the event log deterministically, usually because of non-deterministic workflow code.” DBOS raises DBOSStepNondeterminismError when “a single execution of a workflow recorded different results for the same step.” These are not warnings. They are the run failing after it survived the crash, which is the worst possible outcome: you paid for durability and got a stuck run.
| In orchestration code | Why it breaks replay | Replay-safe alternative |
|---|---|---|
| Reading the clock | A different time on replay changes a branch | Workflow-context time (Temporal); Vercel’s sandbox fixes Date so “the values don’t change across replays”; or read time inside a step |
| Random numbers and UUIDs | Different value on replay, different path | Capture once in a step; Restate’s ctx.rand is “seeded… with the invocation ID, so it always returns the same value on retries”; Vercel fixes Math.random but warns seeded values “are not secret” |
| Network calls, database reads, file I/O | External state differs between runs | Always a step or activity |
| Environment variables and config lookups | Change between deploys | Pass as workflow input or read inside a step |
| Threads, unmanaged concurrency | Ordering of results varies | Engine-provided parallelism (Promise.all over steps, Temporal’s async primitives) |
| A model call | Same prompt, different answer, different tool choice | Always a step or activity, without exception |
Vercel’s approach is worth noting as the exception that proves the rule. Its workflow functions run in a sandbox without full Node.js access, so the compiler can fix the non-deterministic primitives for you. The documentation says: “The sandboxed environment that workflows run in already ensures determinism.” That removes the clock and random pitfalls, but it cannot remove the network, and it cannot make a model deterministic. The model call still has to be a step.
Clocks and random numbers break replay by accident. A model call breaks it by nature. Ask the same model the same question twice and you will get two different answers; ask it which tool to call and you may get two different tools. If the model call lives in orchestration code, a replay after a crash asks the model again, gets a different plan, and tries to match that plan against a history recorded from the first plan. Temporal’s non-deterministic error and Vercel’s replay divergence are what that looks like.
The agent frameworks that integrate with these engines have all reached the same design. Temporal’s OpenAI Agents SDK integration documents that “Agent orchestration… runs inside the Workflow, while model calls run as Activities,” and that model calls “retry durably and are not repeated during Workflow replay.” Its Google ADK integration, as the AI cookbook index describes it, is built so that “every LLM call and every I/O tool call runs as a durable activity.” Vercel’s durable-agent guide introduces WorkflowAgent from @ai-sdk/workflow, which “runs the agent loop inside a workflow, persists state across step boundaries, and lets tool executions marked with "use step" retry automatically.” Inngest’s tool-loop guide wraps the model call in step.run("think", …) and each tool in its own step.run so that “a failing tool doesn’t re-run the LLM call.”
// Inngest agent tool loop (TypeScript), from the vendor guide, trimmed
while (!done && iterations < MAX_ITERATIONS) {
iterations++;
const llmResult = await step.run(`think`, async () =>
anthropic.messages.create({ model, max_tokens: 4096, messages, tools }));
const toolCalls = llmResult.content.filter((b) => b.type === "tool_use");
if (toolCalls.length === 0) { done = true; return { response: text, iterations }; }
messages.push({ role: "assistant", content: llmResult.content });
for (const toolCall of toolCalls) {
const result = await step.run(`tool-${toolCall.name}`, async () =>
executeTool(toolCall.name, toolCall.input));
toolResults.push({ type: "tool_result", tool_use_id: toolCall.id, content: result });
}
messages.push({ role: "user", content: toolResults });
}
Notice the step names. Inngest hashes the step ID to look up the recorded result, and the versioning guide explains that for steps inside loops “the SDK combines the step ID with an occurrence counter.” That is how a loop with the same literal name on every iteration still gets a distinct record per pass. Temporal matches by command sequence rather than by name, which is why reordering commands is a versioning event there.
The five engines differ in where the replay happens and what gets re-executed.
| Engine | Recovery mechanism, per its docs | Where orchestration code runs |
|---|---|---|
| Temporal | Re-runs the Workflow code from the start; Commands are matched against the Event History; Activity results are reused rather than recomputed | Your Workers, polling Task Queues |
| Vercel Workflows | “the system replays execution deterministically from where it stopped”; the workflow function “compiles into a route that orchestrates execution”; a replay attempt longer than 240 seconds may abort the run | Your Vercel Functions, driven by Vercel Queues |
| Inngest | “Each step in your function is executed as a separate HTTP request”; the function is re-invoked with the state of previous executions and the SDK injects memoized results | Your compute, invoked over HTTPS by Inngest |
| Restate | The server “retries by invoking the handler again with the latest journal”; recorded actions are skipped and their responses injected | Your services, proxied by the Restate Server |
| DBOS | “The workflow is called again with its checkpointed inputs. As the workflow re-executes, it checks before each step if that step’s output is checkpointed in Postgres” | Your process; the library talks to your Postgres |
Two practical consequences. The replay cost grows with the history. Vercel’s pricing page notes that runs exceeding 2,000 events or 1 GB “have slower replay times” and recommends child workflows; its event-sourcing documentation says a run “is worth splitting well before it fails: split into child workflows once a run would grow past a few thousand events.” Temporal logs a warning at 10,240 events and terminates the execution past 51,200, which is why its Continue-As-New exists: “Temporal will tell your Workflow when it’s approaching performance or scalability problems.” An agent loop with thousands of iterations needs a plan for history growth from day one.
The second consequence is latency between steps. Each step boundary is a round trip to the engine: a Workflow Task in Temporal, an HTTP request in Inngest, a queue message in Vercel, a Postgres write in DBOS. DBOS’s comparison page claims its checkpoint “typically takes 1-2ms” while a Temporal step dispatch “takes tens to hundreds of ms.” That is a vendor comparing itself to a competitor, so treat the ratio as a claim and measure your own. The point stands independently: step granularity is a latency decision, and module 8 returns to it for the 30-second SLA case.
Agents run for days. You deploy several times a day. The question of what happens to an in-flight run when its code changes is not theoretical, and the engines take three different stances.
Patch markers. Temporal’s Python SDK uses workflow.patched("id"), which “inserts a marker into the Event History”; during replay the Worker “will fail the Workflow task when the Workflow code doesn’t produce the same patch marker.” Once old executions have drained you call deprecate_patch, and only when “pre-patch Workflows have left retention” may you remove both calls. DBOS has the same shape: DBOS.patch("use-baz") “returns True for new workflows… and False for old workflows,” followed by deprecate_patch and removal.
# Temporal Python patching, from the vendor docs
if workflow.patched("my-patch"):
self._result = await workflow.execute_activity(
post_patch_activity, schedule_to_close_timeout=timedelta(minutes=5))
else:
self._result = await workflow.execute_activity(
pre_patch_activity, schedule_to_close_timeout=timedelta(minutes=5))
Pin the run to its code. Vercel Workflows pins by default: “Workflow runs are pinned to the deployment that starts them,” so a run sleeping for two days “does not suddenly resume into the new implementation.” To move a run you cancel it and rerun on the latest deployment, or build a self-upgrading loop where each run “does a bounded piece of work, then starts the next run on the latest deployment and exits,” passing deploymentId: "latest". Temporal’s Worker Versioning, generally available since the Replay conference announcements of 6 May 2026, “pins running Workflows to the Worker version that started them.” DBOS versions too: “When DBOS tries to recover workflows, it only recovers workflows whose version matches the current application version,” and recommends blue-green deployments that drain old versions. Restate’s CLI output says the same about its invocations: those “that made progress on a deployment cannot move to newer deployments automatically.”
Match by step identity. Inngest does neither. On resume “Inngest uses the current code and reuses results from completed steps whose IDs still match.” Adding a step runs it when reached; changing a step’s ID “treats it as a new step and runs it”; reordering returns saved results by ID with a possible warning. The guide’s own caution is the one that matters for agents: changing a step ID on a step that writes to another system “can repeat a payment, email, or resource creation.”
Prompt changes are code changes. If your orchestration decides which tools to expose based on a prompt template, and you edit the template, a replaying Temporal or Vercel run may now issue different commands than its history recorded. Keep prompts and tool lists as step inputs or recorded values, not as constants baked into the orchestration function, and version them like code. Vercel’s pinning spares you the divergence but not the surprise: the pinned run finishes on yesterday’s prompt.
Prices read from Anthropic’s pricing page on 14 October 2026: Claude Opus 5.5 at $4 per million input tokens and $20 per million output tokens; Claude Sonnet 5.5 at $2 and $10.
Design A (model calls in orchestration code). On replay the engine has no record of the 30 model results, so the code calls the model 30 more times before reaching the crash point. Re-billed input: 30 × 30,000 = 900,000 tokens. Re-billed output: 30 × 1,500 = 45,000 tokens. On Opus 5.5: 0.9 × $4 = $3.60 plus 0.045 × $20 = $0.90, total $4.50 per crash. On Sonnet 5.5: $1.80 plus $0.45, total $2.25 per crash. Those are the best case. The likely case is that one of the 30 replayed calls picks a different tool, the command stream no longer matches the recorded tool calls, and the run dies with a non-deterministic or replay-divergence error, which means you paid the $4.50 and still lost the run.
Design B (every call a step). Replay reads 30 model results and 30 tool results from the history and re-runs only call 31. Re-billed tokens: one call, 30,000 input and 1,500 output, which is $0.15 on Opus 5.5 and $0.075 on Sonnet 5.5. The orchestration cost of recording those 60 steps is a rounding error: at Vercel’s $0.02 per 1,000 events with three events per step, 60 steps are 180 events, or $0.0036; at Temporal Cloud’s $50 per million actions, 60 activity executions are $0.003.
The ratio. Design A re-bills about 30 times the tokens of Design B on every crash, and risks a stuck run. The engine’s fee for preventing that is three orders of magnitude smaller than the tokens it saves. Module 6 scales this to a 10,000-tool-call run.
Every model call is a step. Every tool call is a step. No exceptions for “cheap” calls, no exceptions for calls you think cannot fail. The strongest objection is overhead: each step is a round trip and a serialized payload, and payload limits are real. Temporal Cloud caps a payload at 2 MB per request, Inngest caps a step’s returned data at 4 MiB and run state at 32 MiB, Vercel allows 50 MB. The answer is not fewer steps but smaller payloads: pass references, not blobs. Temporal’s cookbook has a claim-check recipe for “offloading large payloads to S3,” and its External Payload Storage, announced 6 May 2026, is described as “especially useful for agentic and AI data processing applications.” An agent’s growing message history is exactly the payload that needs this treatment.
Every engine in this course retries a failed step, and every one of them tells you that the consequence is your problem. Temporal’s Activities page: “We recommend that it be idempotent, so retries can be processed without duplicate side effects.” DBOS’s architecture page: “Steps should be idempotent, meaning it should be safe to retry them multiple times.” Vercel’s errors page: when a retried step “performs external side effects (payments, emails, API writes), ensure those calls are idempotent,” and, in the section on backend connection failures, the sentence every tool author should frame: “A connection failure does not prove that the backend rejected a write: it may have accepted it before the response was lost.”
The defaults differ in how hard they retry, which changes how many duplicates a non-idempotent tool can produce.
| Engine | Default retry behaviour for a step | How to stop retrying | Read |
|---|---|---|---|
| Temporal | Initial interval 1 second, backoff coefficient 2.0, maximum interval 100 times the initial, maximum attempts unlimited (“The default is unlimited”) | List the error type in the policy’s non-retryable types (“By default, none are specified”); Workflows themselves “do not retry by default” | 14 Oct 2026 |
| Vercel Workflows | “steps retry up to 3 times on arbitrary errors” (4 attempts); steps are “enqueued immediately after a failure”; set maxRetries per step | Throw FatalError; use RetryableError with retryAfter for backoff; serialization failures skip retries | 14 Oct 2026 (SDK docs source) |
| Inngest | Steps retried on thrown errors; the steps guide’s sample comments “Can also be retried up to 4 times”; attempts configured per function | Throw NonRetriableError (used in AgentKit’s human-in-the-loop sample) | 14 Oct 2026 (docs source) |
| Restate | “the ctx.run function is retried infinitely on failure” unless a retry policy is set | Throw TerminalError; with a maximum attempts policy the block “fails with a TerminalError” when exhausted | 14 Oct 2026 (docs source) |
| DBOS | Steps re-executed on recovery; retries configurable per step | Raise from the step; mark steps idempotent | 14 Oct 2026 (docs source) |
| LangGraph | “Retries are opt-in. A node retries only when it has a retryPolicy configured”; defaults once set: max_attempts 3, initial_interval 0.5 seconds | Omit the policy, or return false from retry_on | 14 Oct 2026 (docs source) |
Read the first and fourth rows again. Temporal and Restate retry forever by default. A tool that posts a comment to a pull request, wrapped in a step with no retry policy and no idempotency key, will post that comment every time the step times out after the comment landed. That is not a bug in the engine. It is the engine doing what its documentation says it does.
An idempotency key is a value that identifies the logical operation, not the attempt. Inngest’s idempotency guide gives the clearest decomposition of where to deduplicate, and it maps directly onto an agent.
Three places to deduplicate: “At the event producer” with a stable event id that “Inngest uses… to avoid starting functions again for a duplicate event received within 24 hours”; “At the function” with an idempotency expression that “prevents another execution with the same value for 24 hours”; and “At the external service” by sending “a stable operation key to a payment, email, database, or provisioning API,” because “A request can succeed before Inngest records the step result. A retry can then call the API again. Event and function deduplication do not remove that risk.” And the rule for the key itself: “Do not use a new retry attempt ID as the external idempotency key.”
The other engines give you the first two layers under different names. Restate accepts an idempotency-key request header and “will then deduplicate requests with the same idempotency key, and will only execute the handler once.” DBOS: “An assigned workflow ID acts as an idempotency key,” so a repeated ID runs the workflow once, with a workflow_id_reuse_policy of reject if you want duplicates to fail loudly. Temporal’s billing page notes that “De-duplicated Workflow starts that share a Workflow ID do not count as an Action,” which tells you the server deduplicates starts by ID too. None of these protect the third layer. The external service must see a key.
For an agent tool, derive the key from the step’s identity and the operation’s arguments, never from the attempt. A workable recipe: hash the run ID, the step ID (including its loop occurrence) and a canonical form of the tool input. Two attempts of the same step produce the same key; two different tool calls in the same run do not. Pass that key to the external API that supports one, and for APIs that do not, check for the resource first using a deterministic name that embeds the key.
# DBOS: workflow ID as idempotency key, from the vendor tutorial
with SetWorkflowID("very-unique-id"):
example_workflow()
# Restate: idempotent invocation over HTTP, from the vendor docs
curl localhost:8080/GreeterService/greet \
-H 'idempotency-key: ad5472esg4dsg525dssdfa5loi' \
--json '"Hi"'
Three classes of failure need three different responses, and the engines give you a primitive for each.
Transient failures retry with backoff. A rate-limited model API is the canonical case. Vercel’s RetryableError takes a retryAfter duration or date, and its advanced example computes exponential backoff from getStepMetadata().attempt. Temporal’s default policy already doubles the interval each attempt and caps it at 100 seconds. Its AI cookbook has a recipe for “passing retry information from HTTP headers to Temporal’s retry mechanisms,” which is the right way to honour a provider’s retry-after header.
Poison inputs must not retry. A tool asked to open a file that does not exist, a model asked for a schema it cannot produce, a 404 from an API: retrying burns attempts and, under an unlimited policy, burns them forever. Throw the terminal error type: FatalError in Vercel, TerminalError in Restate, NonRetriableError in Inngest, a type listed in Temporal’s non-retryable error types. Vercel also classifies serialization failures as deterministic: a step whose arguments cannot be serialized “fails like a step whose body threw a FatalError” and skips the retry loop. For an agent, a model output that fails schema validation is a poison input to the next step, and the right move is to feed the error back to the model in a new step rather than retry the failed one.
Structural failures end the run. Vercel’s MAX_EVENTS_EXCEEDED fires when a run reaches the per-run event ceiling; Temporal terminates past 51,200 events. No retry policy fixes those. They are design errors, solved by Continue-As-New or child workflows.
Some agent actions have no idempotent form because the world has already changed: an email was sent, a pull request was merged, a payment was captured, a message was posted in a channel where people read it. For these you need two mechanisms: a gate before the action, which is module 4, and a compensation after it if a later step fails, which is the saga pattern.
Temporal’s saga design page is the most precise statement of the pattern: “You implement each step as a local transaction with a corresponding compensation transaction,” and “If any step fails, you execute compensation transactions in reverse order to undo the effects of all completed steps.” Two details separate a saga that works from one that fails on the day it matters.
On timing: “In all SDKs, compensations are registered before Activity execution and run in reverse order of registration,” and therefore “The compensation must be idempotent and handle cases where the forward Activity never executed (no-op).” The recovery guide explains why with a lost-response case: “The Activity throws because it never received a response — yet the booking exists on the vendor side.” Pre-registering “guarantees cancelAppraisal runs during rollback either way; idempotency makes it a safe no-op when the side effect never actually landed.” On failure of the compensation itself: “If a compensation fails, log the error and continue executing remaining compensations,” and “In production, alert for manual intervention on persistent compensation failures.”
Vercel’s documentation shows the same rollback loop but registers the compensation after the forward step succeeds: “Only enqueue a compensation after its forward step succeeds.” The two vendors disagree, and Temporal’s reasoning wins for agents. A tool that opened a pull request and then lost the network response has opened the pull request. If the compensation is registered only on success, the rollback never closes it. Register first, make the compensation a no-op when the forward action did not land, and you cover both cases.
// Vercel Workflow SDK rollback loop, from the vendor docs (trimmed)
export async function placeOrderSaga(orderId: string) {
"use workflow";
const rollbacks: Array<() => Promise<void>> = [];
try {
await reserveInventory(orderId);
rollbacks.push(() => releaseInventory(orderId));
await chargePayment(orderId);
rollbacks.push(() => refundPayment(orderId));
} catch (e) {
for (const rollback of rollbacks.reverse()) { await rollback(); }
throw e;
}
}
Restate bakes compensation into cancellation: “For cancellations, Restate will gracefully stop the handler by executing all compensation actions,” whereas “For kills, Restate will immediately stop the handler without executing any compensation actions.” That distinction is the operational form of the saga: an operator who cancels a runaway agent gets the undo; an operator who kills it does not, and must clean up by hand.
For an agent, the practical taxonomy is short. Actions with a native undo (close the pull request, delete the draft, void the uncaptured payment) get a compensation step registered before the forward call. Actions with no undo (the email, the merged PR, the captured payment, the message a human already read) get a gate before them and a compensating communication after them: a follow-up email, a revert PR, a refund, a correction post. Decide which bucket each tool is in when you write the tool, not during the incident.
Here is the failure that is specific to agents and that none of the vendor pages address head-on. A step calls the model, the model decides to call the send_email tool, the step’s code sends the email, and the process dies before the step result is recorded. The step retries. The model is asked again, decides this time to call search_docs instead, and the run continues. The history now records a search. The email that went out is recorded nowhere. No compensation will ever fire for it, because the saga does not know it happened.
The rule that prevents this is simple: a model call and a side-effecting tool call never share a step. The model call is one step whose recorded result is the tool choice and its arguments. The tool call is a second step whose idempotency key is derived from that recorded choice. A retry of the model step before its result is recorded is harmless because it has no side effect. A retry of the tool step is harmless because the key deduplicates it. Inngest’s tool-loop guide enforces exactly this split, and Temporal’s OpenAI Agents integration does it structurally by routing I/O tools through activity_as_tool() while keeping deterministic tools in the workflow.
A second, subtler form: do not retry a model step because you dislike its answer. A retry is for failures of the call, not failures of judgement. If the output fails validation, record it, and let the orchestration decide to ask again in a new step with the error in context. That is the evaluator-optimizer loop from the dynamic workflows course, and it belongs in the orchestration, where it is replayable, not in the retry policy, where it is invisible.
Every tool with a side effect carries a key derived from the step’s identity, and the step records the external resource’s ID as the very first thing it does after the call returns. That second clause is the one teams skip. The strongest objection is that many third-party APIs do not accept idempotency keys. True, and the fallback is a deterministic resource name that embeds the key plus a check-before-create. It costs one extra read per tool call. Compared with explaining to a customer why an agent sent the same email three times, one read is cheap.
Derive a key from the run ID, the step ID and a hash of the tool input (repository, base branch, title, body digest). Use the key in the head branch name, for example agent/<key>, so the resource name is deterministic. Before creating, list open pull requests from that head branch; if one exists, return its number and URL as the step result instead of creating another. If none exists, create it. Record the pull request number as the first line of the step’s result so a crash after creation but before the record is caught by the next attempt’s check. Register a compensation before the forward call that closes any open pull request from that head branch and is a no-op when none exists. Keep the model call that decided to open the pull request in its own earlier step, so a retry here can never re-ask the model. Finally, treat “merge” as a separate, gated tool: opening is undoable, merging is not.
A wait is where a plain process and a durable engine diverge completely. A process that waits is a process that is running, billed and killable. A durable workflow that waits is a row in a database. Every vendor leads with this.
Temporal: “Workers consume no additional resources while waiting for a Timer to fire,” and timers are persisted so that “even if your Worker or Temporal Service is down” when the period ends, the workflow resumes once they return. Vercel: sleep “will pause the workflow and consume no resources, resuming automatically when the time expires,” with no limit on sleep duration. Inngest: “A Function paused by a sleeping Step doesn’t affect your account capacity; i.e. it does not count against your plan’s concurrency limit,” with sleeps “up to a maximum of one year.” Restate, on serverless: “you don’t pay for the time the handler is sleeping.” DBOS: “DBOS saves the wakeup time in the database so that even if the workflow is interrupted” it still wakes on schedule.
For an agent this is the difference between an approval step that costs nothing for a week and an approval step that holds a container, a connection and a model context in memory for a week. It is also what lets one engine host thousands of paused agent runs on a handful of workers: Temporal’s timers page notes that because waiting is free, one Worker can await a very large number of timers at once.
Each engine exposes two or three ways for the outside world to wake a workflow. The names differ; the shape is the same: the workflow records that it is waiting for something identified by a token, suspends, and resumes with a payload when that token is resolved.
| Engine | Primitive | How it resumes | Deadline |
|---|---|---|---|
| Temporal | Signal (asynchronous), Update (synchronous, returns a value), Query (read-only); workflow.wait_condition blocks on state | Client, CLI or another workflow sends the Signal or Update; the condition is “evaluated on every state transition” | timeout on wait_condition, raising asyncio.TimeoutError |
| Vercel Workflows | defineHook / createHook for typed in-app events; createWebhook for an HTTP callback URL | approvalHook.resume(token, payload) from any route; hooks “can receive multiple payloads while active” | Race the hook against sleep; hook token size 255 bytes; token conflicts raise HookConflictError |
| Inngest | step.waitForEvent with a match or if expression | Any producer sends the event; “you can resume many runs from a single event” | timeout; resolves null when nothing arrives |
| Restate | Awakeable (“the callback (task token) pattern”); durable promises on workflows | External process resolves or rejects over HTTP (/restate/awakeables/<id>/resolve) or via the SDK | Combine with durable timers |
| DBOS | DBOS.send / DBOS.recv messages; set_event / get_event for publishing status | Any code, including a plain HTTP handler, calls DBOS.send(workflow_id, message, topic); delivery is exactly-once from a workflow | timeout_seconds on recv, returning None |
| LangGraph | interrupt(payload) inside a node | Re-invoke the graph with Command(resume=value), which becomes the return value of the interrupt | None built in; the thread simply stays checkpointed |
Two engine-specific cautions from the documentation. Inngest’s wait-for-event page warns that the wait “begins listening for new events from when the code is executed. This means that events sent before the function is executed will not be handled by the wait,” and notes a “lookback” feature is coming; the usage-limits page lists an event lookback period of one hour on the Free plan and three days on Pro. Design the approval UI to send its event only after the wait is registered, or carry the decision in a store the workflow can also read. Vercel’s hooks reserve their token while active, and a second run trying to create a hook with the same token gets a hook_conflict event rather than a hook; derive tokens from the run, not from the business object, if two runs can wait on the same object.
Temporal’s approval design pattern and Inngest’s human-in-the-loop guide describe the same three-outcome shape, and both are explicit that every outcome needs handling.
# Temporal approval pattern (Python), from the vendor design-patterns page
@workflow.signal
def submit_approval(self, data: ApprovalData) -> None:
self.approval_data = data
# inside the run method
try:
await workflow.wait_condition(
lambda: self.approval_data is not None,
timeout=timedelta(seconds=timeout_seconds),
)
except asyncio.TimeoutError:
self.status = "TIMEOUT"
Temporal’s page carries the decision context in the Signal payload, the decision, the approver and comments, rather than a bare boolean, and its escalation variant first waits the initial timeout, sends a notification Activity, then waits again with an extended timeout of 24 hours in the sample. Inngest’s guide lists four timeout strategies in a table: auto-reject for “High-risk actions (delete, deploy, send to external),” auto-approve for “Low-risk, time-sensitive actions,” escalate for actions that “must get a response,” and retry notification when a “Human might have missed the first message.” Its escalation sample waits four hours, pings an escalation contact, and waits four more.
// Inngest human-in-the-loop (TypeScript), from the vendor guide, trimmed
const approval = await step.waitForEvent("wait-for-approval", {
event: "agent/approval.response",
match: "data.approvalId",
timeout: "24h",
});
if (!approval) { return { status: "timed_out" }; } // TIMEOUT
if (approval.data.approved) {
const result = await step.run("execute-action", async () =>
performAction(approval.data));
return { status: "approved", result }; // APPROVED
}
return { status: "rejected", reason: approval.data.reason }; // REJECTED
Vercel’s version uses a typed hook and a loop over received events, so an approver can send “changes” with notes and the workflow revises and re-asks, which is the right shape for a draft-review cycle.
// Vercel Workflows approval hook, from the vendor concepts page, trimmed
export const approvalHook = defineHook<{ decision: 'approved' | 'changes'; notes?: string }>();
export async function aiApprovalWorkflow(topic: string) {
'use workflow';
const draft = await generateDraft(topic);
const events = approvalHook.create({ token: 'draft-123' });
for await (const event of events) {
if (event.decision === 'approved') { await publishDraft(draft); break; }
const revised = await refineDraft(draft, event.notes);
await publishDraft(revised);
}
}
// elsewhere: await approvalHook.resume('draft-123', { decision, notes });
An agent permission gate is an approval wait with the tool call as the thing being approved. The managed harnesses have started to expose exactly this, and the mapping is worth making explicit because it tells you what to build yourself when you are not on a managed harness.
Vercel’s agent approval guide describes a tool marked needsApproval: true that suspends the run, and notes that the flag “also accepts an async function that decides per call from the tool’s input.” On resume, the guide says, “a suspended tool call resumes with the exact message history, tool outputs, and pending decisions” as they stood before the pause, and that “Approvals are recorded as workflow events.” Inngest’s AgentKit shows an ask_developer tool whose handler calls step.waitForEvent with a four-hour timeout, “pausing the execution of the AgentKit network.” Claude Managed Agents handles tool confirmation as session events: the sessions documentation lists user.tool_confirmation among the events a session accepts, and the session status table says a session is idle when the “Agent is waiting for input, including user messages or tool confirmations.” LangGraph’s interrupt is the same idea for a graph node: the interrupt payload describes the proposed action, and Command(resume=...) carries the human’s answer back into the node.
What unites these is that the gate lives in the substrate’s wait primitive, not in the model’s prompt. A prompt that says “ask before sending email” is advice the model can ignore under injection. A tool whose execution path is a durable wait cannot be bypassed by text. The securing agents course makes the architectural argument; the managed agents course covers the product side of permission policies. Here the point is narrower: the durable wait is the mechanism that makes a gate both unskippable and free to hold open.
Design rules that follow. Gate on the recorded tool choice from the model step, so the approver sees exactly what will run and the run cannot substitute a different call after approval. Put the approver’s identity in the payload, as Temporal’s pattern does, so the history is an audit trail. Give every gate a deadline and a default, and make the default for anything irreversible a rejection. Escalate by waiting again, not by looping on a shorter timer.
A recurring agent is a wait with a calendar. The engines give you three shapes.
A timer inside a long-lived workflow: sleep until next Monday, do the work, sleep again. Temporal’s cloud limits allow a timer duration of up to 100 years; Vercel’s sleep has no limit; Inngest’s is one year, or seven days on the Free plan. The catch is history growth: a workflow that loops weekly for a year accumulates events, which is why Vercel’s versioning page models such loops “as a sequence of runs,” each starting the next on the latest deployment, and why Temporal has Continue-As-New.
A schedule object that starts a fresh run each time. Temporal Schedules cost three actions per execution; DBOS scheduled workflows “run your workflows exactly-once per time interval” and are “automatically enqueued to their owning application’s latest version”; Restate’s timers page says “Cron jobs are not yet a built-in feature of Restate” and points to a guide for building one from durable timers and virtual objects.
A managed scheduled deployment. Claude Managed Agents’ scheduled deployments take a cron expression and an IANA timezone with minute granularity, record every attempt as a deployment run with a session_id or an error.type, apply “jitter of up to 15% of the interval between runs, with a minimum of 5 seconds and a maximum of 9 minutes,” do not backfill missed triggers after an unpause, and allow 1,000 deployments per organization. The same page carries the daylight-saving warning every cron user eventually learns: wall-clock times that do not exist on a spring-forward day “are not triggered” and times that occur twice on a fall-back day “fire twice.”
The choice follows the scheduled agents course logic: a schedule that starts short, idempotent runs is the bookmark design and is right for most recurring work; a long-lived timer loop is right only when the run must carry state forward that is awkward to re-derive, and then it needs a Continue-As-New or next-run boundary.
An approval is a durable wait with a deadline and a default outcome, enforced by the substrate, and never a prompt instruction. The strongest objection is latency: a durable wait is a round trip through the engine, and a chat agent with a 30-second SLA cannot afford a queue hop on every tool call. The answer is that the gate is not on every tool call. It is on the handful of tools that are irreversible, and those are precisely the ones where a few hundred milliseconds of engine latency is irrelevant next to the minutes a human takes to decide. Module 8 draws the line for the support-agent case.
Model call to draft is a step; the draft is the recorded result. Notify the primary approver in a step with an idempotency key, then wait on an approval event or signal correlated by the run ID with a 48-hour timeout. On timeout, a step notifies the backup approver, then wait again on the same correlation with a 24-hour timeout. On the second timeout, record the outcome as discarded and end the run; the default for publishing is reject because publishing cannot be undone. On approval, a step publishes with a key derived from the run and draft hash; on “changes” with notes, a new model step revises and the wait repeats with a fresh deadline. Put the approver’s identity and comments in the event payload so the history is the audit trail. Hosting: a scheduled deployment or cron starts a fresh run each Monday; the 72 hours of waiting cost nothing, and the run survives any deploy in between because it is pinned or patched per module 2.
Five products sell durable execution for application code in October 2026, and they differ more in where the engine lives than in what it does. Temporal is a server that your Workers poll, available self-hosted or as Temporal Cloud, with SDKs its developer page lists for “.NET, Go, Java, PHP, Python, Ruby, Rust, and TypeScript.” Vercel Workflows is the hosted form of the open-source Workflow SDK, generally available on Vercel since 16 April 2026, where “each step runs as its own function invocation on Fluid compute” and the state lives in Vercel’s managed persistence. Inngest is an event-driven platform that invokes your functions over HTTP and memoizes each step; its README names SDKs for TypeScript/JavaScript, Python, Go and Kotlin/Java, with the server “available under the Server Side Public License” with delayed Apache 2.0 publication and the SDKs under Apache 2.0. Restate is a single Rust binary that proxies invocations to your services and journals their progress, with SDKs for TypeScript, Java and Kotlin, Python, Go and Rust, licensed under the Business Source License 1.1. DBOS is a library: “DBOS provides lightweight durable workflows built on top of Postgres,” MIT-licensed, for TypeScript, Python, Go and Java, with an optional control plane called Conductor.
Money followed the category this year. Temporal announced on 14 September 2026 a $550 million Series E at a $12.55 billion valuation, led by Lightspeed. Restate’s own site could not be read for this course, but Forkast and Remio reported a $20 million Series A led by Singular on 30 September 2026; treat that as reported, not verified. Vercel’s GA post framed the product as eliminating a category: “Workflows eliminates the orchestrator entirely,” and described dedicated orchestration services as “distributed systems you pay for on top of your core application compute.” That sentence is the thesis of the hosted-in-your-platform camp, and DBOS makes the same argument from the library side: “The only point of failure in DBOS is Postgres.”
| Engine | Programming model, per its docs | Where orchestration and steps run | Languages | License and hosting |
|---|---|---|---|---|
| Temporal | Workflow Definitions and Activities; Commands matched against an Event History on replay; Signals, Queries, Updates, Timers, Child Workflows, Schedules | Your Workers poll Task Queues; Temporal Server or Temporal Cloud stores history. Serverless Workers on AWS Lambda announced 6 May 2026 as pre-release | .NET, Go, Java, PHP, Python, Ruby, Rust, TypeScript (docs, 14 Oct 2026) | Self-host or Temporal Cloud; 99.9% SLA, 99.99% with HA options |
| Vercel Workflows | "use workflow" and "use step" directives compiled by a build plugin; event-sourced run log; sleep, hooks, webhooks, streams | Workflow and step code run as Vercel Functions, dispatched by Vercel Queues; state in managed persistence pinned to one region per run | TypeScript/JavaScript; Python via the vercel Python SDK (docs, 14 Oct 2026) | Open-source Workflow SDK (workflow 5.2.2 on npm; recent versions list Apache-2.0) with Worlds for local, Vercel, Postgres or custom backends; hosted on Vercel |
| Inngest | step.run, step.sleep, step.waitForEvent, step.invoke, step.ai; functions triggered by events, crons or webhooks; memoized step results | Your compute, invoked over HTTPS (“serve” for serverless, “connect” for workers); Inngest Cloud or a self-hosted server | TypeScript/JavaScript, Python, Go, Kotlin/Java (README, 14 Oct 2026) | Server under SSPL with delayed Apache 2.0; SDKs Apache 2.0; cloud plans Hobby, Pro, Enterprise |
| Restate | Services, Virtual Objects with per-key K/V state and single-writer concurrency, and Workflows whose run handler executes once per ID; ctx.run journals side effects; awakeables and durable timers | Your services behind the Restate Server, which “runs as a single binary with zero dependencies”; Lambda and other serverless targets supported | TypeScript, Java, Kotlin, Go, Python, Rust (docs source, 14 Oct 2026) | BSL 1.1 for the server; Restate Cloud (“Free to start”) and BYOC, per the archived cloud page of 30 Sep 2026 |
| DBOS | @DBOS.workflow() and @DBOS.step() decorators; every workflow input and step output checkpointed to a Postgres system database; queues, messages, events, streams | In your own process; recovery is coordinated by Conductor or by the single node at startup | Python, TypeScript, Go, Java (docs and pricing FAQ, 14 Oct 2026) | DBOS Transact library, MIT; Conductor control plane paid; DBOS Cloud hosting optional |
Two structural facts fall out of that table. First, only Temporal and Restate put a long-lived server on the critical path of every step; Vercel and Inngest put a queue and an HTTP hop there; DBOS puts a Postgres write. That is the latency axis module 8 uses for the 30-second SLA case. Second, the hosting choice is mostly made for you by where your code already runs: an application on Vercel has Workflows one import away, a Postgres shop has DBOS one decorator away, and a polyglot organization with platform engineers has Temporal’s eight SDKs.
| Engine | Pricing unit | Run length and history cap | Versioning in-flight runs | Observability |
|---|---|---|---|---|
| Temporal Cloud | Actions: $50 per million pay-as-you-go, down to $25 per million self-serve; active storage $0.042 per GB-hour, retained $0.00105 per GB-hour; Business from $500 per month (read 14 Oct 2026) | Timers up to 100 years; Event History warns at 10,240 events, terminates at 51,200 events or 50 MB; Continue-As-New to go longer; retention 1 to 90 days | Patch markers (patched / deprecate_patch) or Worker Versioning, GA May 2026, which pins runs to the Worker version that started them | Event History per execution in the UI; replay from history; Braintrust integration (20 Jan 2026) maps every Workflow and Activity to a span; OpenMetrics support |
| Vercel Workflows | Events $0.02 per 1,000; data written $0.50 per GB; data retained $0.50 per GB-month; Hobby includes 50,000 events; functions and queues billed separately (page updated 16 Sep 2026) | No maximum run or sleep duration; 25,000 events and 10,000 steps per run; 50 MB payload; 2 GB per run; replay attempt must finish within 240 seconds | Runs pinned to their deployment by default; cancel and rerun on latest, or self-upgrading runs with deploymentId: "latest" | Dashboard trace viewer redesigned 23 Jun 2026; time-between-steps measurement 5 Aug 2026; local npx workflow web; retention 1, 7 or 30 days by plan |
| Inngest | Executions, where “one run uses 6 executions (the run itself + 5 steps)” for a five-step function; Hobby $0 with 50k; Pro from $99 per month with 1M included, pay-as-you-go to 20M (archived pricing page, 9 Sep 2026) | 1,000 steps per function; sleep up to one year; maximum run length 30 days Free, 90 days Basic, 366 days Pro; run state 32 MiB; step output 4 MiB (archived limits page, 30 Aug 2026) | Step-ID matching: completed steps with the same ID return saved results; new IDs run; major rewrites get a new function ID with a cutover trigger | Traces and logs retained 24 hours Free, 7 days Pro, 90 days Enterprise on the pricing page; Dev Server with rerun-from-step; step.ai records prompts and tokens |
| Restate | Cloud is “Usage-based pricing with volume discounts” and “Free to start” per the archived cloud page; the pricing page itself did not render in the archived copy, so no unit price is stated here | No per-invocation numeric caps found in the documentation read; ctx.run retries infinitely by default | Invocations bound to the deployment they progressed on and “cannot move to newer deployments automatically” | UI and CLI invocation inspection with journal progress and retry status; OpenTelemetry traces exported to your platform |
| DBOS | Library free; Conductor Pro $99 per month with 1M checkpoints and $50 per extra million; Teams $499 per month with 10M checkpoints and $40 per extra million (archived pricing page, 24 Aug 2026) | No per-workflow step cap stated in the docs read; durable sleep persisted in Postgres | Patch markers (DBOS.patch) or application versions with blue-green rollout; recovery only to matching versions | Conductor dashboards, workflow fork from a step, Prometheus-compatible metrics; SQL introspection of system tables |
Every vendor shipped agent-specific features this year, and reading them together shows the category converging on the same four needs: model calls as steps, streaming out of a durable run, approval gates, and large payloads.
Temporal’s Replay conference announcements of 6 May 2026 included Workflow Streams, “durable streaming that uses Temporal’s Signal & Update primitives” for LLM tokens and live updates; External Payload Storage, “especially useful for agentic and AI data processing applications”; Standalone Activities; and integrations with Google ADK and the OpenAI Agents SDK, the latter now generally available with a pre-release sandbox integration in which “all sandbox operations — LLM model API calls, the full sandbox lifecycle, running commands — execute as Temporal activities.” Its AI cookbook lists recipes for Claude and OpenAI tool-calling loops, a durable MCP server, Strands, the Vercel AI SDK, a human-in-the-loop agent, a claim-check pattern and a multi-agent deep-research system.
Vercel shipped WorkflowAgent in @ai-sdk/workflow (2.0.72 on npm as of 14 October 2026), which runs the AI SDK agent loop inside a workflow with tools as steps, resumable streams through getWritable(), and needsApproval gates. Its guides cover a durable code agent that runs generated tests in a Sandbox microVM and a research agent, and the platform added run-region pinning on 20 July 2026.
Inngest offers step.ai.infer(), which “offloads the inference request to Inngest’s infrastructure, pausing your function execution until the request finishes,” so a serverless function is not billed while the model thinks; step.ai.wrap() to turn an existing SDK call into a step with prompt and token observability; and AgentKit (0.13.2 on npm), a TypeScript framework for “multi-agent networks with deterministic routing and rich tooling via MCP” that gains retries, offloading and traces when deployed with Inngest. Its documentation now has a “Durable Agents” section with the tool-loop, human-in-the-loop and sub-agent delegation guides quoted throughout this course.
Restate positions Virtual Objects for “Stateful agents, actors, and digital twins: e.g. AI chat sessions,” where per-key state and single-writer concurrency give each conversation a serialized, durable mailbox. Its cloud page headline in the 30 September 2026 archive is “The Durable Runtime for your Agents and Backends,” and search results point to blog posts on OpenAI Agents SDK and Google ADK integrations in January 2026, which this course could not fetch directly.
DBOS ships a Hacker News research agent as a documented example in Python and TypeScript, publishes a case study with Yutori on “Large-scale, Durable Agentic AI,” and its workflow streaming API, where writes from a workflow “happen exactly-once” and writes from a step “at-least-once,” is built for streaming model output out of a durable run.
Below the engines sits a tier of tools that persist an agent’s conversation but not a replayable step ledger. They are often enough, and knowing exactly what they do and do not give you prevents both over- and under-engineering.
| Tool | What persists | Crash in the middle of a tool call | Waits |
|---|---|---|---|
| LangGraph checkpointers | Graph state at every super-step, per thread; InMemorySaver for experiments, SqliteSaver, PostgresSaver, MongoDB and Redis savers for production | Depends on the durability mode: "exit" saves only when the graph exits, so “you cannot recover from system failures (like process crashes) mid-execution”; "async" has “a small risk” of a lost checkpoint; "sync" writes “every checkpoint before continuing.” Node retries are opt-in | interrupt() pauses the thread; resume with Command(resume=…); no deadline primitive |
| Claude Managed Agents sessions | “Claude Managed Agents is stateful by design: sessions are long-running, resume cleanly after pauses, and store conversation history, sandbox state, and outputs server-side.” Event history “is persisted server-side and can be fetched in full” | Statuses include rescheduling, “Transient error occurred, retrying automatically”; the harness owns the loop, so you do not write replay code and cannot inspect a step ledger as you would a Temporal history | Tool confirmations and user messages leave the session idle at no model cost; session budgets pause work with budget_reached; scheduled deployments for cron |
| Claude Agent SDK sessions | Transcript on disk; continue resumes the most recent session, resume takes a session ID, fork “creates a new session that starts with a copy of the original’s history” (archived docs, 9 Oct 2026) | Resuming returns “with full context from wherever the session left off”; the interrupted turn itself is not replayed as steps | Permission prompts and hooks within a query; no durable timer |
| OpenAI Agents SDK sessions | “Sessions store conversation history for a specific session”; SQLite, SQLAlchemy, Redis, MongoDB, Dapr and server-side Conversations implementations | Not described; the docs cover resuming after an approval pause with the same session instance, not recovery from a process failure. Temporal’s integration notes that SQLiteSession is unsupported there because it stores history in a local file | Approval pauses resumed with the same session; no durable timer |
The pattern across the tier: the conversation survives, the in-flight step does not. For a chat agent whose every tool is idempotent and whose longest wait is a user typing, that is enough, and the Claude Agent SDK course and managed agents course show how far the managed tier goes. The moment a run must sleep for days, fan out and join, or compensate, you are back in engine territory, and the cleanest designs put a managed session inside an engine step rather than pretending the session is the engine. Temporal’s sandbox integration is one worked example of exactly that composition.
| Package | Version | Registry |
|---|---|---|
@temporalio/workflow / temporalio | 1.24.0 / 1.34.0 (released 30 Sep 2026) | npm / PyPI |
workflow / @ai-sdk/workflow | 5.2.2 / 2.0.72 | npm |
inngest / @inngest/agent-kit / inngest (Python) | 4.24.0 / 0.13.2 / 0.5.19 | npm / npm / PyPI |
@restatedev/restate-sdk / restate_sdk | 1.17.3 / 1.0.5 | npm / PyPI |
@dbos-inc/dbos-sdk / dbos | 5.2.11 / 3.2.0 (released 29 Sep 2026) | npm / PyPI |
@langchain/langgraph / langgraph | 1.5.0 / 1.2.15 (released 12 Oct 2026) | npm / PyPI |
openai-agents | 0.23.1 (released 2 Oct 2026) | PyPI |
Pick by where your code already runs, then check the two constraints that actually bite: history growth and language. An application on Vercel should use Workflows and split long agent loops into child runs before 2,000 events. A Postgres shop that will not add infrastructure should use DBOS and buy Conductor the day it runs more than one executor. A polyglot organization with a platform team should use Temporal and budget engineering time for the determinism contract. A TypeScript team that already thinks in events and needs concurrency, throttling and debounce as first-class features should use Inngest and design around the 1,000-steps-per-function limit. A team building stateful per-user agents with strict latency should look hard at Restate’s Virtual Objects. The strongest objection is that this ignores feature depth, and Temporal’s depth is real. The answer is that an engine your team can operate beats a deeper engine it cannot, and the vendors have converged enough on the agent features in this module that depth rarely decides.
(1) Vercel Workflows: the code already runs on Vercel, WorkflowAgent gives tools-as-steps, and a hook is the review. Split the loop into child runs if it grows past a few thousand events. (2) DBOS: decorators on existing functions, checkpoints in the Postgres already there, scheduled workflows for nightly runs, Conductor when a second executor appears. (3) Temporal: three languages on one history, Signals for signatures, timers for the days of waiting, Worker Versioning for the long tail of in-flight documents, and an enterprise support tier. (4) Restate Virtual Objects, or a durability-lite session with idempotent tools: per-key state delivered with the request and single-writer semantics fit a mailbox per customer, and a sub-second budget rules out a queue hop per message. Reasonable people would also put (4) on Inngest with concurrency keys per customer, or on DBOS if its checkpoint-latency claim holds in your measurement. The point of the exercise is that none of the four answers is “the best engine”; each is “the engine nearest the code.”
| Engine | What you pay for | Numbers as read | Date and source |
|---|---|---|---|
| Temporal Cloud | Actions: workflow starts, activity starts and retries, timers, signals, queries, updates, schedules; plus storage | From $50 per million actions pay-as-you-go, down to $25 per million self-serve, further by commit; active storage $0.042 per GB-hour; retained storage $0.00105 per GB-hour; Business plan the greater of $500 per month or 10% of usage, including 2.5M actions; $150 of free credits for 90 days | 14 Oct 2026, temporal.io/pricing and the Actions reference |
| Vercel Workflows | Events, data written, data retained; functions and queues billed separately | $0.02 per 1,000 events; $0.50 per GB written; $0.50 per GB-month retained; Hobby includes 50,000 events and 1 GB written per month; a step that succeeds first time “produces three events” | Page updated 16 Sep 2026, read 14 Oct 2026 |
| Inngest | Executions: “a single durable function run plus each step inside it”; concurrent steps; events; span data | Hobby $0 with 50k executions and 5 concurrent steps; Pro from $99 per month with 1M executions included, “Pay as you go up to 20M,” 100 concurrent steps then $25 per 25; Enterprise custom; overage billed “at tiered rates” without a listed price | Archived copy of inngest.com/pricing captured 9 Sep 2026 |
| Restate Cloud | Usage-based on actions | “Free to start — no credit card” and “Usage-based pricing with volume discounts”; no unit price readable. A third party, DevTune, listed on 10 July 2026 a free tier of 50k actions per month and overage from $25 per million; unverified against Restate’s own page | Archived copy of restate.dev/cloud captured 30 Sep 2026 |
| DBOS | Library is free; Conductor meters checkpoints (“workflows, steps, transactions”) and seats | Pro $99 per month: 2 seats, 3 apps, 1M checkpoints, $50 per extra million; Teams $499 per month: 10 seats, 10 apps, 10M checkpoints, $40 per extra million; Enterprise custom with self-hosted Conductor | Archived copy of dbos.dev/pricing captured 24 Aug 2026 |
| Claude Managed Agents | Model tokens at list price, session running time, web searches | Running time $0.08 per hour; web searches $10 per 1,000; budgets enforced “between model requests, not mid-request” | 14 Oct 2026, session budgets page |
| Limit | Temporal Cloud | Vercel Workflows | Inngest |
|---|---|---|---|
| History or events per run | Warning at 10,240 events or 10 MB; execution terminated past 51,200 events or 50 MB | 25,000 events per run; 10,000 steps per run; 2 GB total entity storage; slower replay past 2,000 events | 1,000 steps per function; run state 32 MiB |
| Payload per step | 2 MB per request, including each argument and return value; 4 MB per gRPC message | 50 MB per input or output | 4 MiB returned by a step; event size 256 KiB Free to 3 MiB Pro |
| Signals and hooks | 10,000 Signals and 2,000 Updates per execution; 2,000 concurrent incomplete Activities | Hook token 255 bytes; 200 hook creations per second | Event lookback 1 hour Free, 3 days Pro |
| Run duration and sleep | Timer up to 100 years; retention 1 to 90 days after close | No limit on run duration or sleep; replay attempt 240 seconds; step bounded by function limits (300 s default, 800 s max, 1,800 s beta) | Sleep up to 1 year (7 days Free); run length 30 days Free, 90 days Basic, 366 days Pro; step timeout up to 2 hours |
| Read | 14 Oct 2026 | 14 Oct 2026 | Archived 30 Aug 2026; note the limits page lists a Basic plan that the September pricing page does not |
Restate and DBOS publish no comparable per-run caps in the documentation read for this course, and DBOS’s only hard number in this area is Conductor’s executor timeout: “By default, the executor timeout is 60 seconds, so Conductor waits 60 seconds after an executor disconnects before recovering its workflows.” That number matters more than it looks: it is the floor on how long a crashed agent stays dead before another executor picks it up.
Read the first row as a design constraint. An agent that loops 10,000 times with a model call and a tool call per iteration produces 20,000 steps. On Temporal that is several tens of thousands of events, past the termination limit several times over, so the loop must Continue-As-New periodically; the OpenAI Agents integration page recommends exactly that for long conversations. On Vercel it is 60,000 events against a 25,000 cap and 20,000 steps against a 10,000 cap, so the loop must become a chain of child workflows. On Inngest it is twenty functions of a thousand steps each, or a fan-out. None of this is hard. All of it must be designed before the first production run, because the failure mode is a dead run at step 9,999.
| Model | Input per million tokens | Output per million tokens | Cache read per million | Batch (50% off) |
|---|---|---|---|---|
| Claude Opus 5.5 | $4 | $20 | $0.20 | $2 / $10 |
| Claude Sonnet 5.5 | $2 | $10 | $0.10 | $1 / $5 |
| Claude Haiku 5.5 (prompts up to 100,000 tokens) | $0.10 | $0.50 | $0.01 | $0.05 / $0.25 |
| Claude Haiku 5.5 (prompts over 100,000 tokens) | $0.50 | $2.50 | $0.05 | $0.25 / $1.25 |
Source: Anthropic’s pricing page, which also states that cache hits on Opus 5.5 and Sonnet 5.5 “are priced at 0.05x the base input price.” The inference economics course goes deeper on token costs; here the prices exist to be set against the orchestration fee.
(a) Tokens. Input: 10,000 × 20,000 = 200,000,000 tokens, or 200 million. Output: 10,000 × 1,000 = 10,000,000, or 10 million.
(b) Orchestration.
(c) The ratio. Orchestration costs between $0.50 and $2 for the run. Tokens cost $25 on Haiku, $158 to $500 on Sonnet, $316 to $1,000 on Opus. The engine’s fee is roughly 0.05% to 0.6% of the token bill on Opus, 0.1% to 1.3% on Sonnet, and 2% to 8% on Haiku. Even on the cheapest model the engine is a rounding error. What the engine actually sells is not its fee but the Design B saving from module 2: one crash in a Design A run re-bills up to the whole token column.
Three conclusions follow from the arithmetic, and each one changes a decision.
First, never choose an engine on its per-step price. The spread between the most and least expensive orchestration above is about a dollar on a run that costs hundreds in tokens. Choose on hosting fit, limits, language and operability, as module 5 argued.
Second, the engine’s job is to protect the token column. Every crash under a naive design re-bills tokens; every retry of a step that wraps both a model call and a tool call re-bills a model call. Step granularity is a cost control, not an overhead. The loop engineering course treats the loop’s shape; this module adds that the shape determines the re-bill.
Third, prompt caching changes the shape of the exercise more than the engine does. Moving from no caching to 90% cache hits cut the Sonnet run from $500 to $158. Caching depends on a stable prefix, and a replayed step that reconstructs the message history byte-for-byte is exactly what keeps the prefix stable. Durable execution and caching are allies.
A 10,000-iteration loop that should have stopped at 50 is the agent-era version of a fork bomb, and the only reliable stop is a cap enforced below the model’s reasoning. The substrates offer three shapes.
A hard dollar cap on a managed session. Claude Managed Agents session budgets are “a hard ceiling on the session’s list cost”; the platform “stops issuing new model requests once that running total reaches max_list_cost,” the session pauses and goes idle with budget_reached, and because enforcement is between requests the final cost “can land a fraction past the cap.” Deployments copy the cap onto every run they start. The page also distinguishes this from the Messages API’s task budgets, “which are advisory, token-denominated budgets the model uses to self-regulate.” Advisory budgets are for pacing; hard budgets are for stopping.
# Claude Managed Agents: hard budget at session creation, from the vendor docs
session = client.beta.sessions.create(
agent=agent.id,
environment_id=environment.id,
budget={
"type": "limit",
"max_list_cost": {"amount": "125", "currency": "USD"}, # cents, as a string
},
)
A counter in workflow state. On an engine, keep the running token total in the orchestration state, add each model step’s recorded usage to it, and exit the loop when it crosses the line. Because the total is derived from recorded step results it is replay-safe and survives the crash. Inngest’s tool-loop guide shows exactly this with a 500,000-input-token ceiling and an iteration cap, and adds a stuck-loop detector that breaks when the last three tool calls repeat. Temporal has no native dollar budget; the counter is the pattern.
A cap on the trigger. For scheduled agents, cap at the schedule: the scheduled agents course puts a per-run budget on a daily brief, and Managed Agents deployments do the same with a per-run budget object. A cap per run bounds the blast radius of one bad day; a cap per month bounds the quarter.
Compute for steps is billed by whoever runs the step. On Vercel that is function active CPU and provisioned memory; on Temporal it is the Workers you operate or, since May 2026 in pre-release, Lambda; on Inngest and Restate it is your own compute; on DBOS it is your process and your Postgres. A model call that waits 30 seconds for a response is 30 seconds of a function or worker doing nothing, which is why Inngest’s step.ai.infer() offloads the request so “you don’t pay for function execution while waiting,” and why Vercel bills active CPU rather than wall time.
Retention is a cost and a capability. Temporal Cloud retains closed histories for 30 days by default, configurable from 1 to 90; Vercel keeps run data for 1, 7 or 30 days by plan; Inngest keeps traces for 24 hours on Hobby and 7 days on Pro; DBOS Conductor manages retention per application. If your audit requirement is longer than the retention, export: Vercel’s own guide suggests writing run history to Blob “as a final step for audits that outlast retention.”
People are the largest hidden cost. The determinism contract is a tax on every engineer who touches the orchestration, and the review burden of “is this call a step” never goes away. That tax is the strongest honest argument for the durability-lite tier where it suffices.
Put a hard spending cap below the model on every long-running agent, and make it a stop, not a warning. A managed session budget, a replay-safe token counter, or a per-run cap on the schedule; any of the three, but one of them, and set at a level that would hurt if hit ten times in a day. The strongest objection is that caps truncate legitimate long tasks. They do, and the fix is a resume-with-raised-cap path, which Managed Agents provides by updating the budget and which an engine provides by a signal that raises the counter’s ceiling. A paused run with a human deciding whether to spend more is the correct state for an agent that has already spent more than you expected.
An agent’s run history is a better trace than anything you will get from a logging library, because it is the engine’s own record of what happened, in order, with the inputs and outputs that the engine needed to replay. Every vendor has built its debugging surface on that record.
Vercel’s dashboard records, per its documentation, “Every step, input, output, sleep, and error inside a workflow,” and the redesigned trace viewer of 23 June 2026 lets you “click into any step to see its inputs, outputs, and run metadata” and zoom any section of the timeline. The 5 August 2026 addition measures the gap between any two steps, which the changelog says “can be useful for spotting delayed starts and queue waits when a run takes longer than its steps account for.” That gap is exactly where an agent’s wall-clock time hides: not in the model call, but in the queue hop between the model step and the tool step. The run data is sensitive, which is why the docs note that “Team owners can read decrypted Workflow run data” and offer a “Workflow Run Data Viewer” permission for others.
Temporal’s Event History is viewable per execution and, as module 2 covered, replayable: the Python Replayer recreates a run from its history and “Replay succeeds only if the Workflow Definition is compatible with the provided history.” Restate’s CLI shows the journal as progress: restate invocations describe prints each recorded call with a check mark, the current status such as backing-off with the retry count and next retry time, and a reminder that the invocation “is bound to run on deployment” it started on. DBOS Conductor provides “dashboards of all active and past workflows and all queued tasks as well as real-time workflow visualization,” and lets an operator “pause any workflow execution, start any stopped or enqueued workflow, or restart any workflow from a specific step.” That last capability, which DBOS calls fork, is the operational superpower of a step ledger: “if a large number of billing workflows fail overnight due to an outage in a payment API, you can use fork to restart them all from the payment step after the outage is resolved.” Inngest’s Dev Server offers the same move for agents, letting you “edit prompts and rerun from the step” when the model call was made with step.ai.wrap and JSON-serializable arguments.
Claude Managed Agents keeps its own ledger: “Event history is persisted server-side and can be fetched in full,” and the session object carries usage.list_cost and usage.active_seconds. What it does not give you is a step ledger you can replay in your own process, which is the trade of the managed tier.
A durable run and an LLM observability tool each see half the story. The run history knows which step ran, how many times, and what it returned. The model trace knows the prompt, the tokens, the latency and the cost. The join key is the run identity, and the integrations that exist in 2026 are all built on that join.
“every Temporal Workflow and Activity becomes a Braintrust span.” The integration provides a “Full trace hierarchy: From client request through every Workflow step,” with inputs, outputs, token counts and latency for each LLM call, the prompt version each execution used, and cost per query, while on the Temporal side you keep “Workflow executions: every research request with full Event History,” activity retries and execution state. The setup is a plugin on the Worker and the Client: plugins=[BraintrustPlugin()].
Inngest’s step.ai.wrap() records the model call as a step “augmenting the observability of your Inngest Functions with information such as prompts and tokens used,” so the join is built in. Vercel’s WorkflowAgent writes ModelCallStreamPart chunks to the run’s persistent stream, and the run error code is exposed “as an OpenTelemetry span attribute (workflow.error.code).” Restate “exposes traces via OpenTelemetry” with an invocation ID that “lets you correlate logs and metrics.” DBOS Conductor exposes metrics “from a Prometheus-compatible endpoint” and, for sensitive workloads, a metadata-only mode in which “only workflow and step metadata (but not data, like workflow or step inputs or outputs) is sent to Conductor.”
If you are not using a vendor integration, do the join by hand with two rules. Every model-call span carries the run ID, the step ID and the attempt number as attributes, so a trace can be found from a history. Every model step’s recorded result carries the provider request ID and the span ID, so a history can be found from a trace. The agent tracing course covers the span conventions; the addition here is that the step result is the right place to persist the link, because it is the one record the engine guarantees to keep.
A durable run cannot crash in the old sense, so the failure mode moves. The new failure is the run that is alive and not progressing: retrying forever against a poison input, waiting for an event nobody will send, paused at a budget nobody noticed, or pinned to a deployment that no longer exists. Define “stuck” as a run older than its longest legitimate wait with no registered wait in its history, and alert on that definition per workflow type.
The engines surface the states you need. Vercel lists REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED and DEPLOYMENT_MISMATCH among its run error codes, and npx workflow inspect runs --status running --json lists live runs. Restate shows backing-off with “Retried 141 time(s)” in its own CLI example, which is what a poison input looks like under an infinite retry policy. Inngest’s sleeps page documents a known limitation worth an alert of its own: runs sleeping longer than the plan’s trace retention “are still sleeping and will resume as scheduled, even if they’re not visible in the Function Runs list,” so a dashboard that shows nothing does not mean nothing is waiting. DBOS Conductor marks a disconnected executor DEAD after the 60-second default timeout and recovers its workflows elsewhere; Teams plans add “Custom workflow alerting.” Managed Agents sessions report rescheduling for transient retries and budget_reached when a cap pauses work, and the status docs warn that “idle alone doesn’t mean that the work is done” when a workflow run is open.
Four alerts cover most agent incidents: the age of the oldest running run per workflow type against its expected duration; the retry count of any single step above a threshold, which catches poison inputs before the bill does; any compensation failure, which per Temporal’s saga guidance warrants “manual intervention on persistent compensation failures”; and the count of runs paused at a budget or approval beyond their deadline. Each of those is a query over the history or the status field, not a log search.
Deploying under live agent runs is a scheduled operation, and the engines’ versioning models from module 2 turn into runbook steps here.
Before a deploy, list the in-flight runs by version. DBOS gives that query directly, shown below, with the guidance “When deploying a new version of your code, launch new processes running your new code version, but retain some processes running your old code version” while they drain. Temporal’s Worker Versioning, GA since May 2026, keeps in-flight workflows on their original version so a rollout can proceed while old Workers finish. Vercel pins runs to deployments and warns about the consequence on rollback: “a rollback does not stop runs on the deployment you rolled back from. A run whose steps keep failing on that deployment is retried there… until the run fails or is cancelled.” And the sentence that should be in every runbook: “runs pinned to a deleted deployment never complete or fail on their own, so cancel them as well.”
# DBOS: which runs are still on the old version? From the vendor docs
active = DBOS.list_workflows(
app_version="1.0.0",
status=["ENQUEUED", "DELAYED", "PENDING"],
)
if not active:
print("Safe to retire version 1.0.0")
Inngest’s migration guidance is the most hands-on, because its step-ID model means old runs resume on new code: test both paths in the Dev Server by starting a run, pausing it with step.sleep(), changing the code, resuming, and checking “which saved results it reuses and which steps it runs.” For a rewrite, deploy a second function with mutually exclusive triggers and a cutover timestamp, and remove the original “Once those runs finish.” Restate’s deployment binding means the same thing operationally: keep the old deployment registered until its invocations complete.
For agents specifically, treat a prompt or tool-list change as a version bump and run the drain procedure. A pinned run finishing on an old prompt is acceptable; a replaying run diverging on a new prompt is an incident.
Two test types matter for durable agents and both are cheap once the engine is in place.
Time-skipping tests the waits. Temporal’s Python test suite starts a time-skipping server with start_time_skipping() where “Timers, which include sleeps and conditional timeouts, are fast-forwarded except when Activities are running,” and env.sleep(seconds) advances the clock manually. A 48-hour approval timeout and its 24-hour escalation run in milliseconds, with mocked Activities that “should have the same signature as the real implementation.” Inngest’s equivalent is the Dev Server paused-run procedure above.
# Temporal Python: time-skipping and replay tests, from the vendor docs
async with await WorkflowEnvironment.start_time_skipping() as env:
await env.sleep(3) # advance time by 3 seconds
replayer = Replayer(workflows=[YourWorkflow])
await replayer.replay_workflow(WorkflowHistory.from_json(history_json_str))
Replay tests test the determinism contract. Export histories of real runs, including runs with model calls, and replay them against the current orchestration code in CI. If the orchestration asks for a different command than the history recorded, the test fails before production does. Temporal’s versioning page says to “incorporate Replay Testing” to check whether a patch is needed; the same histories double as golden tests for the agent’s control flow, because the recorded model outputs are fixtures that never change. This is the cheapest regression suite an agent team will ever own, and it is free once every model call is a step.
Local development is a solved problem on every engine. Vercel: npx workflow web opens “a local dashboard showing all workflow runs and their status, as well as a trace viewer,” with a bundled local backend that needs no setup. Inngest: npx inngest-cli dev runs the Dev Server. Temporal: temporal server start-dev. Restate: the server “runs as a single binary with zero dependencies” locally. DBOS: a local Postgres. The agent-specific addition is to record model step outputs in development and replay them in tests, so the suite never calls a model.
Replay is the debugger and the run ID is the correlation key. Every orchestration change ships with a replay test against histories from production, and every model span carries the run and step that made it. The strongest objection is that histories contain customer data and prompts, which makes a CI fixture a data-handling problem. True, and the answer is the pattern DBOS calls metadata-only and Vercel enforces with viewer permissions: redact payloads in exported histories, keep the step structure, and replay against that. A determinism test needs the shape of the history, not its contents.
One: pull the histories of three of the forty and look at the last recorded event; a step with a high attempt count is a poison input, a registered wait with no deadline is a lost signal, a DEPLOYMENT_MISMATCH or version error is a stranded deploy, and a budget pause is a cap. Two: if it is a poison input, stop the bleeding by cancelling the retry loop for those runs (Restate cancel runs compensations, kill does not; Vercel and Temporal cancel from the UI or CLI) and record the input for a terminal-error fix. Three: if it is a lost signal, check whether the approval event was sent before the wait registered (Inngest’s documented race) or to a hook token another run owned (Vercel’s hook_conflict), then resend the signal with the correct correlation. Four: if it is a stranded deploy, find which deployment or version the runs are pinned to, and either restore it long enough to drain or cancel and rerun on latest with the same inputs. Five: before touching anything irreversible, check which side effects each run has already recorded, so that a rerun starts after the last external write, not before it. Then fix the alert that let forty runs age six hours before anyone looked.
The shape: a user asks for a change; the agent reads the repository, edits, runs tests in a sandbox, iterates, and opens a pull request. Inngest’s tool-loop guide sizes it: “a coding agent that reads, edits, and tests might need 15–20” iterations. Runs last minutes to an hour. The irreversible actions are push, pull-request creation and merge.
Substrate. If the application is on Vercel, Vercel Workflows with Sandbox: Vercel’s own guide builds exactly this loop, with generateCode, generateTests and executeInSandbox as steps, the generated code running “in an ephemeral Firecracker microVM with its own filesystem and network,” and a FatalError for non-retryable failures. If the organization is polyglot or already runs Temporal, Temporal with its OpenAI Agents SDK sandbox integration, where “LLM model API calls, the full sandbox lifecycle, running commands — execute as Temporal activities” and a Worker crash mid-turn means “Temporal replays the workflow and resumes from the last completed state.” Either way the loop is short enough that history limits are not a concern; a 20-iteration run is well under any cap.
Steps. Every model call; every sandbox command batch, because a test run is slow, flaky and worth retrying; repository reads, because they are I/O; pull-request creation, with the idempotency scheme from module 3’s exercise. Not steps. Prompt assembly and output parsing. Vercel’s guide keeps its stripCodeFences helper and the loop control out of steps, and that is right: they are deterministic functions of recorded results. Gate. Merge, always; push to a shared branch, usually. Compensation. Close the pull request, registered before creation. Budget. An iteration cap in the loop and a token counter in state. The coding agents course covers the agent itself; this is the substrate underneath it.
The shape: every Monday, gather sources, synthesize a report, send it for review, wait up to 48 hours, publish or discard. Runs are mostly waiting. The irreversible action is publishing.
Substrate. This is the case the scheduled agents course solves with a cron plus a bookmark, right up to the approval. The approval is the forcing function from module 1, so pick one of three. A Claude Managed Agents scheduled deployment with a per-run budget, if you want the harness hosted and the approval handled through its tool-confirmation events; the deployment records every attempt, and the budget copies onto each session. Inngest, if the team is TypeScript and event-driven: a cron trigger, step.run for fetch and synthesis, step.waitForEvent with a 48-hour timeout, a second wait for escalation, and a run length well inside the plan limits. DBOS, if the team is on Postgres: a scheduled workflow that runs “exactly-once per time interval,” DBOS.recv with a timeout for the approval, and set_event to publish status to a review page.
Steps. Each source fetch, with large documents offloaded by reference so step payloads stay under the engine’s limit; each model call; the send-for-review notification with a key; the publish with a key. Not steps. Formatting and templating. Gate. Publish, with reject as the timeout default. Budget. Per run, on the schedule.
The shape: a conversation, many short turns, tools that look up orders and occasionally change them, an answer expected within seconds. The irreversible actions are refunds, cancellations and outbound messages.
Substrate. Not a queue-hopping engine on the hot path. A step boundary is a round trip, and DBOS’s comparison page, which is a vendor’s claim about a competitor, puts a Temporal step dispatch at “tens to hundreds of ms” against its own “1-2ms” Postgres write; whatever the true numbers are in your environment, measure them before putting ten steps between a question and its answer. The conversation itself belongs to the durability-lite tier: a managed session, an Agent SDK session, or a LangGraph thread with a PostgresSaver in sync mode if you can afford the write per super-step. Make every read tool idempotent and every write tool keyed, and the conversation survives a restart at the cost of one lost turn.
Then spawn durable work for the long tail. A refund is not a 30-second operation; it is a saga with a gate. Start it as a workflow from the chat turn and return a reference, the way DBOS’s checkout example starts a workflow under an idempotency key and returns a payment ID through get_event. Restate’s Virtual Objects are the natural fit if you want per-customer durable state with single-writer semantics on every turn: “Stateful agents, actors, and digital twins: e.g. AI chat sessions” is the vendor’s own example, and its invocations are pushed over streaming connections rather than polled. Gate. Refund and outbound message. Budget. Per conversation. The managed agents course and the realtime course cover the conversational layer.
| Position | Strongest objection | Answer |
|---|---|---|
| Every model call is a step | Step overhead and payload limits: 2 MB on Temporal Cloud, 4 MiB per step on Inngest, 50 MB on Vercel | Shrink payloads, not step counts. Claim-check large context by reference; Temporal’s External Payload Storage exists for this. A crash under the alternative re-bills every call since the start and risks divergence. |
| Do not put the agent loop in a serverless function without an engine | Functions now run 800 seconds, or 1,800 in beta; most agent runs fit | Duration is not the problem; deploys and waits are. A function dies on deploy with no history, and it cannot wait for a human. Vercel’s own limits page routes “unlimited execution time” to Workflows. |
| A cron plus a bookmark is enough until you need waits | Engines are cheap and the demo is impressive; start there | The determinism contract is a tax on every engineer forever. Pay it when the wait, the fan-out or the compensation appears. |
| Gates live in the substrate, not the prompt | A durable wait on every tool call wrecks latency | Gate only irreversible tools. Human decision time dwarfs engine latency on exactly those tools. |
| Register compensations before the forward call | Vercel’s docs say to enqueue a compensation only after the forward step succeeds | The lost-response case: the action landed, the response did not. Temporal’s guide pre-registers and makes the compensation an idempotent no-op; that covers both outcomes. |
| A hard spending cap sits below the model | Caps truncate legitimate long tasks | Pause, do not kill, and provide a resume-with-raised-cap path. A human deciding whether to spend more is the right state for an overrun. |
| Choose the engine nearest your code | Feature depth differs, and Temporal’s is deepest | An engine the team can operate beats a deeper one it cannot, and the vendors converged on the agent features in 2026. Orchestration fees never decide; they are a few percent of tokens at most. |
Six layers, each with a named owner and a question that decides whether it is done.
| Layer | What lives here | Done when |
|---|---|---|
| 1. Harness | The agent’s control flow: chains, routing, orchestrator-worker, evaluator-optimizer, from the dynamic workflows course; iteration caps; stuck-loop detection | The loop is a deterministic function of recorded step results and nothing else |
| 2. Substrate | The engine (Temporal, Vercel Workflows, Inngest, Restate, DBOS) or the durability-lite session, chosen by where the code runs | A kill of the process mid-run resumes at the step in flight, demonstrated in staging |
| 3. Steps | Model calls, tool calls, compensations; each with a stable ID, a payload under the engine’s limit, and for side effects a key derived from step identity | No step contains both a model call and a side effect; every side-effecting step records the external ID first |
| 4. Gates | Durable waits on recorded tool choices for every irreversible action, with deadline, default and escalation | Every gate has three handled outcomes and the irreversible default is reject |
| 5. Observability | Run ID on every model span, trace link in every model step result, alerts on age, retries, compensation failures and paused runs; replay tests in CI | A stuck run can be diagnosed from its history alone within five minutes |
| 6. Budgets | A hard cap per run or session below the model, a per-schedule cap, a resume path | The cap has been hit in staging on purpose and the resume path exercised |
Security sits beside the stack rather than in it: sandboxes, egress control and credentials from the securing agents course apply to the step runtime, and the gate layer is where the two courses meet. The harness shape comes from the opinionated harness course; the substrate’s job is to make whatever shape you chose survive.
Days 1 to 7: inventory and bookmark. List every agent in production with its longest run, its longest wait, its irreversible tools and its hosting. Any agent with no wait longer than its process lifetime gets a bookmark, a post-then-record rule and a per-run budget, and leaves the engine conversation. Pick one agent with a real wait as the pilot. Choose the engine nearest its code by the module 5 rule.
Days 8 to 14: the pilot, every call a step. Move the pilot’s loop into the engine with every model call and tool call as a step, stable step IDs, and payloads under the limit by reference. Kill the process mid-run in staging and watch it resume. Write the first replay test from that staging history.
Days 15 to 21: keys, gates and compensations. Give every side-effecting tool a key derived from step identity. Put a durable wait with a deadline and a reject default in front of every irreversible tool. Register a compensation before each undoable action and make it an idempotent no-op. Test the timeout and escalation paths with time-skipping.
Days 22 to 30: operations. Add the run ID to every model span and the trace link to every model step result. Stand up the four alerts. Write the deploy-and-drain runbook for your engine’s versioning model and rehearse a rollback with runs in flight. Set the hard budget, hit it on purpose, and exercise the resume path. Put the replay suite in CI. Then, and only then, migrate the second agent.
Durable execution is the correct substrate for any agent that waits, and the wrong first project for a team that has never written replay-safe code. Start with one agent, one engine, one wait, and insist on the two tests that make the contract real: a kill mid-run that resumes, and a replay against a production history that passes. The strongest objection is that this is slow, and that a platform team could wrap the engine so product teams never see the determinism contract. Wrappers help and the vendors’ agent SDKs are good ones; but someone on the team must understand why a model call cannot live in orchestration code, because the day the wrapper leaks, that person is the one who reads the history and finds the divergence.
1. Can you name its longest wait and whether that exceeds its process lifetime? 2. Is every model call a recorded step? 3. Is every tool call a recorded step, separate from the model call that chose it? 4. Does every side-effecting tool send a key derived from step identity? 5. Does every irreversible tool sit behind a durable wait with a deadline and a reject default? 6. Is every undoable action paired with a compensation registered before the call? 7. Have you killed the process mid-run in staging and watched it resume? 8. Does a replay test run in CI against a production history? 9. Can you find the model trace for any step from the run history, and the run from any trace? 10. Is there a hard spending cap below the model with a resume path?
Scoring. Ten yes answers: you are done, migrate the next agent. Seven to nine: you have a durable agent with a gap; the missing items are usually 6, 8 and 9, in that order. Four to six: you have a retrying agent; module 2 and 3 are the work. Three or fewer: you have a script, and the first question to answer is whether it needs a wait at all, because if it does not, a bookmark and a budget will get you to six yes answers in a week.