How Do You Make an Agent Survive for Days? Build Durable Long-Running Workflows
An agent that works for thirty seconds can get away with a lot.
It can keep most of its state in memory.
It can assume the process will stay alive.
It can hold a Python object containing the plan.
It can retry a failed tool call directly.
It can wait synchronously for a result.
It can even use the conversation transcript as an approximate record of what has happened.
Then you ask the same system to work for six hours.
Or two days.
Or until a human approves something tomorrow morning.
Or until a deployment finishes.
Or until a customer replies.
Or until a cloud job emits an event.
Or until a retry window opens again.
Now the architecture changes.
The problem is no longer merely:
How should the model reason?
It becomes:
How does the work remain correct while
processes restart,
workers disappear,
providers fail,
intent changes,
state changes,
humans go offline,
and external systems finish whenever they finish?
That is a workflow problem.
The core rule for this post is:
Make the workflow durable. Treat the model and worker as disposable.
This is the point where production agents stop looking like chat loops and start looking like distributed workflow systems with reasoning components inside them.
The Search Problem: “How Do I Build a Long-Running AI Agent?”
A common first implementation looks like this:
async def run_agent(goal):
plan = await model.plan(goal)
for step in plan:
result = await execute(step)
if result.needs_human:
await wait_for_human()
if result.failed:
await retry(step)
return "done"
This is understandable.
It is also fragile.
What happens if the process restarts while waiting for the human?
What happens if the worker dies after the external API accepted the request but before the result was persisted?
What happens if the workflow sleeps for three hours and the original deployment has been cancelled?
What happens if the approval arrives twice?
What happens if a timer fires after a superseding intent has replaced the original goal?
What happens if the model release changes while the workflow is paused?
What happens if a retry runs on a different worker in another region?
What happens if the task has an active commitment that must still be released after the business goal was cancelled?
The answer cannot be:
“Hopefully the LLM remembers.”
The model is not the durable state store.
The worker process is not the durable state store.
The prompt is not the durable state store.
The workflow runtime must be.
A Long-Running Agent Is Two Systems
It helps to split the architecture explicitly.
DURABLE WORKFLOW PLANE
intent versions
goal state
commitments
waits
timers
retries
approvals
events
checkpoints
operation identities
compensation / reconciliation
│
▼
REASONING / EXECUTION PLANE
models
search
retrieval
critics
tools
sandboxes
workers
verifiers
The workflow plane answers:
What is supposed to happen next?
What has already happened?
What are we waiting for?
What remains valid?
What obligations still exist?
The reasoning plane answers:
Given the current durable state,
what candidate action or decision should we produce now?
Those are different responsibilities.
The first should be deterministic whenever possible.
The second may be probabilistic.
That separation is fundamental.
The Workflow Must Survive the Worker
Suppose an agent is performing a release:
1. inspect repository
2. propose migration
3. run tests
4. request approval
5. wait for approval
6. deploy
7. verify production
If step 4 happens at 16:55 and the reviewer responds at 09:10 the next morning, the process that generated the request should not need to remain alive overnight.
The durable system should instead persist something like:
@dataclass(frozen=True)
class WorkflowState:
workflow_id: str
workflow_version: int
intent_id: str
intent_version: int
current_node: str
status: str
active_commitment_ids: tuple[str, ...]
pending_waits: tuple[str, ...]
completed_activity_ids: tuple[str, ...]
checkpoint_ref: str | None
behavioral_release_id: str
policy_version: str
Then the worker may disappear.
Tomorrow, another worker receives the approval event.
It loads the durable state.
It validates:
- current intent;
- current state freshness;
- current authority;
- approval identity;
- behavioral-release compatibility;
- outstanding commitments;
- verifier requirements.
Only then does it continue.
The continuation does not depend on process memory.
Durable Does Not Mean “Serialize the Conversation”
A transcript is useful provenance.
It is not sufficient workflow state.
Consider:
Assistant: I am waiting for approval.
What does that mean operationally?
Which approval?
For which candidate?
Bound to which artifact hash?
Who may approve?
When does it expire?
What intent version authorized the request?
What happens on timeout?
What commitment was created by asking the reviewer?
What state changes invalidate it?
The durable representation must make those facts explicit.
For example:
@dataclass(frozen=True)
class ApprovalWait:
wait_id: str
workflow_id: str
approval_request_id: str
candidate_hash: str
intent_id: str
intent_version: int
required_role: str
expires_at: datetime
timeout_policy: str
state_version_vector: dict[str, str]
Now “waiting for approval” is a real system state.
Workflow Nodes Should Be Semantic
Do not make the durable workflow track every token generation or every internal reasoning step.
Persist semantic boundaries.
Useful workflow nodes include:
GATHER_EVIDENCE
BUILD_CANDIDATE
VERIFY_CANDIDATE
REQUEST_APPROVAL
WAIT_FOR_APPROVAL
REVALIDATE
COMMIT_OPERATION
VERIFY_POSTCONDITION
RECONCILE
COMPLETE
This aligns with the checkpoint principle from Step 35.
The durable system should know where it is in the business and control process.
It does not need to preserve every hidden computational detail of how the model arrived there.
Activities Are Disposable Attempts
A durable workflow invokes activities.
Activities may execute on disposable workers.
For example:
async def inspect_repository_activity(ctx): ...
async def run_tests_activity(ctx): ...
async def generate_patch_activity(ctx): ...
async def verify_patch_activity(ctx): ...
async def deploy_activity(ctx): ...
The durable workflow owns the lifecycle.
The activity owns one attempt.
That distinction is similar to the operation/attempt identity from Step 20.
logical activity
│
├── attempt 1 → timeout
├── attempt 2 → worker crash
└── attempt 3 → success
The workflow should not interpret three physical attempts as three logical operations.
Idempotency Is Mandatory at Durable Boundaries
Long-running workflows are retried.
That is not optional.
Networks fail.
Workers crash.
Messages are delivered more than once.
Schedulers replay work.
Therefore an activity that creates side effects should carry a stable logical operation identity.
@dataclass(frozen=True)
class ActivityCommand:
workflow_id: str
activity_id: str
operation_id: str
attempt_id: str
intent_version: int
fencing_epoch: int
If the same logical operation is retried, the external mutation path must be able to recognize it.
same operation_id
+ new attempt_id
is a retry.
It is not permission to create a second side effect.
Waiting Is a First-Class Workflow State
A long-running agent spends enormous amounts of time not computing.
It waits for:
- humans;
- webhooks;
- scheduled times;
- provider recovery;
- asynchronous jobs;
- approvals;
- CI;
- repository updates;
- market windows;
- business hours;
- rate-limit reset;
- external confirmation.
Do not hold a worker while waiting.
Persist the wait.
RUNNING
↓
WAITING_FOR_EVENT
↓
worker released
↓
[event arrives]
↓
workflow reactivated
This is a major operational difference between a durable workflow and a long-running coroutine.
Timers Must Also Be Durable
Suppose the system says:
retry after 30 minutes
A sleeping process is not a durable timer.
The timer belongs to workflow state.
@dataclass(frozen=True)
class DurableTimer:
timer_id: str
workflow_id: str
due_at: datetime
purpose: str
intent_version: int
policy_version: str
When it fires, that does not mean “perform the action.”
It means:
wake workflow
↓
reload authoritative state
↓
validate current intent
↓
validate current policy
↓
validate freshness
↓
continue if still valid
A timer is a wake-up event.
It is not timeless authorization.
Delayed Work Must Revalidate Intent
Step 37 made intent supersession explicit.
That becomes critical here.
Imagine:
10:00 schedule deployment retry for 11:00
10:30 user cancels deployment
11:00 timer fires
The timer event must not resurrect the old objective.
The durable workflow checks:
if event.intent_version != intent_store.current_version(event.intent_id):
return WorkflowDecision.SUPERSEDED
The same applies to:
- delayed queues;
- scheduled jobs;
- retries;
- human approvals;
- external callbacks;
- resumptions after outage.
External Events Need Stable Identity
Events may arrive twice.
They may arrive late.
They may arrive out of order.
They may refer to an old workflow generation.
A durable event should include stable correlation identity.
@dataclass(frozen=True)
class WorkflowEvent:
event_id: str
workflow_id: str
event_type: str
external_ref: str | None
observed_at: datetime
payload_hash: str
The workflow runtime should record consumed event IDs.
Duplicate delivery should be harmless.
Events Are Evidence, Not Commands
This distinction matters.
Suppose GitHub emits:
CI completed successfully
That event does not automatically mean:
deploy now
The event updates workflow evidence.
The workflow then reevaluates:
current intent?
current release?
approval still valid?
state still fresh?
verifier sufficient?
authority still granted?
Then it decides what transition is allowed.
External events should not bypass the control plane.
Human Review Is Just Another Durable Wait — With Authority
Human approval is special because it affects authority.
But operationally it is still a durable wait.
REQUEST_APPROVAL
↓
WAITING_FOR_APPROVAL
↓
reviewer response
↓
validate response
↓
revalidate candidate/state/intent
↓
continue or invalidate
The model should not sit in a loop asking:
Has the human responded yet?
The workflow should sleep until an authenticated event arrives.
A Human Response Must Be Bound to the Object Reviewed
Step 29 already established this principle.
Durable workflows make it unavoidable.
Suppose the reviewer approves candidate hash:
sha256:abc123
While waiting, another process changes the candidate.
The eventual approval cannot authorize:
sha256:def456
The workflow needs an explicit transition:
APPROVAL_RECEIVED
↓
candidate hash mismatch
↓
APPROVAL_INVALIDATED
↓
REQUEST_NEW_APPROVAL
Durability without identity binding would merely preserve stale authority more reliably.
Retries Belong to the Workflow Policy
Do not bury retry behavior inside every tool implementation.
A workflow should make retry semantics observable.
@dataclass(frozen=True)
class RetryPolicy:
max_attempts: int
initial_delay_seconds: int
max_delay_seconds: int
retryable_errors: tuple[str, ...]
non_retryable_errors: tuple[str, ...]
But advanced agents need more than generic exponential backoff.
A failure may mean:
transient infrastructure failure
capacity unavailable
state conflict
intent superseded
verification failure
policy denial
competence failure
invalid candidate
ambiguous external outcome
Only some of these should retry.
A stale-state conflict should often trigger replan.
A policy denial should not retry.
An ambiguous side effect should reconcile before retry.
A verifier failure may require a different verifier path.
Retry Is Not Replanning
This distinction matters.
retry
= attempt the same logical operation again
replan
= choose a different strategy or action
If a deployment API times out after accepting the deployment, blindly replanning may create a duplicate deployment.
If the implementation itself is invalid, retrying the same command is pointless.
The workflow needs to know which category it is dealing with.
Persist Retry History
Retry policy should have memory outside the model.
@dataclass(frozen=True)
class ActivityAttempt:
activity_id: str
attempt_number: int
started_at: datetime
finished_at: datetime | None
outcome: str
error_class: str | None
external_operation_id: str | None
This prevents:
worker restarts
↓
retry count forgotten
↓
retry forever
It also supports incident forensics and SLO measurement.
Durable Workflow State Is Not Agent Memory
These are different things.
Workflow state contains operational truth:
current node
pending waits
intent version
commitment IDs
completed activities
operation IDs
approval state
retry counts
Agent memory contains information that may help reasoning:
prior observations
summaries
preferences
retrieved evidence
learned heuristics
Do not make memory the authoritative workflow state.
Memory may be stale.
Memory may be summarized incorrectly.
Memory may be unavailable on another placement.
The workflow must remain correct without trusting model memory.
Model Context Should Be Reconstructed
When a workflow wakes after twelve hours, do not assume the original conversation context should simply be replayed wholesale.
Reconstruct a bounded context from durable state.
For example:
current intent
current goal
active commitments
latest authoritative observations
current workflow node
relevant completed activities
pending decision
current policy constraints
Then retrieve only the evidence needed for the next reasoning step.
This reduces context bloat and avoids carrying stale assumptions merely because they appeared earlier in the transcript.
A Workflow Can Outlive a Behavioral Release
Suppose a workflow begins under:
release R17
It waits two days.
Production is now on:
release R18
What happens?
There are several options:
resume under R17
migrate workflow to R18
restart from a durable boundary
require human review
cancel / reconcile
The correct answer depends on compatibility.
This is why Step 24’s behavioral release model and Step 35’s checkpoint compatibility matter.
The workflow must not silently wake under new behavior and pretend it is the same continuation.
Workflow Versioning Is Separate From Behavioral Release Versioning
A workflow definition can change even if the model does not.
For example:
v1:
verify → approve → deploy
v2:
verify → security scan → approve → deploy
That is a workflow-schema change.
Long-running instances need migration policy.
@dataclass(frozen=True)
class WorkflowDefinitionRef:
workflow_name: str
workflow_version: int
Never assume all historical instances should interpret new code automatically.
Deterministic Workflow Definitions Are Valuable
The runtime should prefer explicit transition logic.
class ReleaseWorkflow:
def next(self, state, event):
if state.status == "WAITING_FOR_APPROVAL":
return self.handle_approval(state, event)
if state.status == "WAITING_FOR_CI":
return self.handle_ci(state, event)
...
The model may help decide:
- which candidate is best;
- how to repair a failure;
- what evidence to gather;
- which plan is promising.
But the model does not need to decide whether an already-consumed approval event should be consumed again.
That is workflow logic.
Use the Model at Decision Points, Not as the Clock
A healthy architecture looks like:
workflow reaches decision point
↓
assemble current context
↓
invoke model / search / critic
↓
produce structured candidate decision
↓
validate
↓
persist transition
↓
release worker
Not:
model runs forever
and remembers what time it is
Commitments Must Survive Workflow Suspension
Step 38 introduced durable commitments.
This becomes one of the most important reasons for durable workflow state.
Suppose a workflow reserves a deployment window and then waits six hours.
The commitment remains active even though no worker is running.
workflow suspended
≠
commitment suspended
The commitment ledger should remain queryable by:
- monitoring;
- cancellation logic;
- admission control;
- resource allocation;
- human operators;
- other agents.
Workflow Completion Requires Commitment Closure
A workflow should not report success while leaving unresolved commitments behind.
A completion invariant could be:
def can_complete(state, commitment_store):
unresolved = commitment_store.unresolved_for_workflow(state.workflow_id)
return not unresolved
Or, more realistically, terminal state may allow known deferred obligations:
COMPLETE
COMPLETE_WITH_DEFERRED_COMMITMENTS
RECONCILIATION_REQUIRED
The important thing is that unresolved obligations are explicit.
Cancellation Is a Workflow Transition
Cancellation is not task.cancel().
The workflow should enter a durable cancellation path.
ACTIVE
↓
CANCELLATION_REQUESTED
↓
stop optional work
↓
fence future mutations
↓
inspect commitments
↓
reconcile in-flight operations
↓
release / compensate obligations
↓
CANCELLED
This incorporates Step 37’s intent semantics.
A cancelled business objective may create more workflow work before the system is safely finished.
Cancellation Should Be Idempotent
Users click twice.
Systems retry messages.
Operators issue the same cancellation repeatedly.
The transition should be stable.
cancel(cancelled_workflow)
→ still cancelled
and must not create duplicate release or compensation operations.
Long-Running Workflows Need Protected Cleanup Capacity
Imagine the system is overloaded.
The admission controller begins rejecting new speculative work.
That is reasonable.
But a workflow may still need to:
- release a reservation;
- reconcile a payment;
- verify whether a deployment occurred;
- revoke a temporary capability;
- compensate an external mutation.
Cleanup and reconciliation work should have protected capacity.
This follows the same logic as Step 16’s protected verification reserve.
optional exploration
can be shed
required reconciliation
must retain a completion path
Workflow Deadlines Are Different From Activity Timeouts
Suppose an API activity has a 30-second timeout.
The business workflow may have a 24-hour deadline.
Those are different.
activity timeout
= how long one execution attempt may run
workflow deadline
= when the overall obligation becomes unacceptable
There may also be:
approval deadline
commitment deadline
freshness deadline
external SLA deadline
Treat them separately.
Deadline Expiry Is an Event, Not Automatic Failure
When a deadline passes, the correct behavior depends on policy.
Possible transitions:
ESCALATE
CANCEL
RELEASE_COMMITMENT
REPLAN
DEGRADE
HUMAN_REQUIRED
BREACHED
A model should not improvise this policy from prose.
Workflow State Should Be Append-Only Enough to Audit
A simple mutable row may tell you the current state.
It may not tell you how you got there.
A production workflow benefits from a transition log:
@dataclass(frozen=True)
class WorkflowTransition:
transition_id: str
workflow_id: str
from_state: str
to_state: str
trigger_type: str
trigger_ref: str
intent_version: int
occurred_at: datetime
policy_version: str
Then the current state can be reconstructed or checked against the event history.
This aligns naturally with Step 13 observability and Step 25 replay.
Event Sourcing Is Useful, but Not Mandatory
Do not turn this into architecture fashion.
You may use:
- an event-sourced workflow log;
- durable state rows plus append-only transitions;
- a workflow engine’s native persistence;
- a transactional state machine.
The invariant matters more than the technology:
After process loss, the workflow can reconstruct what has happened and determine the next legal transition without relying on lost model memory.
Exactly-Once Execution Is Still Mostly a Mirage
A durable workflow engine can give strong execution semantics internally.
It cannot magically make every external system exactly-once.
Suppose:
workflow → payment API
The API may accept the payment and the response may be lost.
The durable workflow knows it attempted the call.
It still needs:
operation identity
idempotency key
external status query
reconciliation
Durability does not remove distributed ambiguity.
It makes ambiguity survivable.
Side Effects Should Live Behind Activities
Keep workflow transition logic pure when possible.
workflow decision
↓
activity command
↓
side effect
↓
recorded result
↓
workflow transition
This makes replay much safer.
Replaying workflow logic should not accidentally resend emails or redeploy production.
Replay the Workflow Without Replaying the World
Step 25 distinguished recorded observation replay from live revalidation.
The same rule applies here.
A forensic replay should consume recorded activity outcomes:
activity A returned X
approval event Y arrived
commit operation Z produced result R
It should not reissue the side effects merely because the workflow function is replayed.
This is one of the strongest reasons to separate orchestration from activities.
Workflow Determinism Has a Specific Meaning
You do not need the model to be deterministic.
You need workflow history interpretation to be deterministic enough that replaying the same recorded transitions does not invent a different operational past.
Model calls should appear as recorded activity results or versioned decision artifacts.
For example:
MODEL_DECISION
candidate_id = C42
release = R17
prompt_hash = ...
output_hash = ...
The workflow does not regenerate C42 during ordinary history replay.
A Model Retry Is a New Attempt, Not Historical Replay
If you intentionally call the model again, that is new work.
Record it as such.
model attempt 1 → candidate C41
model attempt 2 → candidate C42
Do not silently overwrite the previous decision artifact.
This is important for trajectory observability and causal analysis.
Durable Workflows Need Explicit Wait Types
Useful wait categories include:
WAIT_TIMER
WAIT_HUMAN
WAIT_EXTERNAL_JOB
WAIT_RESOURCE
WAIT_DEPENDENCY_HEALTH
WAIT_EVENT
WAIT_APPROVAL
WAIT_RETRY_WINDOW
WAIT_RECONCILIATION
Each should have:
- identity;
- creation reason;
- expiry/deadline;
- intent binding;
- relevant state versions;
- wake-up conditions.
This is far safer than one generic sleeping=True flag.
Waiting Can Change Competence and Authority
A workflow that was valid yesterday may not be valid today.
During a long wait:
- the behavioral release may change;
- the verifier may become unavailable;
- the workload may move outside the competence envelope;
- the error budget may burn;
- policy may tighten;
- the authority class may change.
Therefore wake-up logic should not simply continue from the next line.
It should pass through the relevant gates again.
Wake-Up Is a Revalidation Boundary
A good general pattern is:
WAKE EVENT
↓
load durable workflow state
↓
validate intent
↓
validate workflow-version compatibility
↓
validate state freshness
↓
validate commitments
↓
validate competence
↓
validate authority
↓
validate placement
↓
continue
Not every wake-up needs every expensive check.
Use dependency-aware invalidation from Step 36.
But conceptually, wake-up is a revalidation boundary.
The Workflow Should Know Why It Is Waiting
Consider two workflows both paused for one hour.
One is waiting because:
provider rate limit
The other because:
human approval required
Those have completely different semantics.
The workflow state should say why the system is paused.
That affects:
- alerting;
- SLO attribution;
- escalation;
- cancellation;
- budget accounting;
- authority.
Long-Running Agents Need Liveness Invariants
Correctness is not only about preventing bad actions.
A workflow can also fail by never finishing.
Useful invariants include:
no ACTIVE workflow without an owner or pending wait
no WAITING workflow without a wake-up condition
no retry path without a finite policy or escalation
no reconciliation state without protected capacity
no active commitment without a servicing path
These are deterministic checks.
They do not require an LLM.
Detect Orphaned Workflows
A workflow may become orphaned if:
- the queue event was lost;
- a timer was never registered;
- a worker died before persisting the next transition;
- an external callback was mis-correlated;
- a release migration failed;
- a commitment still exists but the workflow says complete.
Periodically scan durable workflow state for impossible or stale combinations.
For example:
def find_orphans(workflows, waits, commitments):
...
Operational reconciliation is part of the architecture.
Workflow SLOs Are Not Model SLOs
A model may respond successfully while the workflow is unhealthy.
Track things like:
workflow completion rate
workflow age
wait age
stuck-workflow count
retry amplification
commitment breach rate
reconciliation backlog
human wait time
wake-up latency
stale-intent wakeups
orphan rate
These belong beside the behavioral reliability metrics from Step 27.
Measure Time by State
For a long-running workflow, total latency alone is not very informative.
Break it down:
reasoning time
external job time
human wait time
capacity wait time
retry delay
reconciliation time
verification time
Then you know what actually makes the workflow slow.
Do not optimize the model because humans take eight hours to approve the result.
Human Wait Time Is an Operational Dependency
Step 33’s dependency graph already allowed human approval capacity as a dependency.
Long-running workflows make that measurable.
If critical workflows routinely block for six hours on one specialist reviewer, that is a real capacity constraint.
The architecture may need:
- reviewer pools;
- delegation;
- escalation;
- better evidence packets;
- narrower approval scope;
- different authority policy.
This is not primarily a model-quality problem.
Workflow Backpressure Matters
If a dependency is unavailable, workflows accumulate.
Suppose a verifier outage causes 20,000 workflows to enter:
WAIT_VERIFIER
When the verifier returns, waking all 20,000 immediately may recreate the outage.
Wake-up itself needs scheduling and backpressure.
dependency recovers
↓
controlled reactivation
↓
priority / deadlines / cohorts
↓
verifier protected from surge
This connects directly to Steps 21 and 22.
Durable Workflow ≠ Workflow Engine Everywhere
Not every agent needs this.
If your operation lasts 15 seconds and has no asynchronous dependencies or consequential side effects, a normal function may be enough.
Use the simplest mechanism that solves the failure.
You likely need durable workflow semantics when you have several of:
hours/days of elapsed time
human waits
external asynchronous jobs
multiple side effects
retries across process restarts
commitments
superseding intent
distributed workers
migration between placements
strict audit/replay requirements
Do not add a workflow engine because durable systems sound sophisticated.
A Minimal Durable Workflow Runtime
The core can be surprisingly small conceptually.
from dataclasses import dataclass
from enum import Enum
class WorkflowStatus(str, Enum):
RUNNABLE = "runnable"
WAITING = "waiting"
RECONCILING = "reconciling"
COMPLETE = "complete"
FAILED = "failed"
CANCELLED = "cancelled"
@dataclass(frozen=True)
class WorkflowRecord:
workflow_id: str
workflow_version: int
intent_id: str
intent_version: int
state_name: str
status: WorkflowStatus
fencing_epoch: int
behavioral_release_id: str
@dataclass(frozen=True)
class TransitionRequest:
workflow_id: str
expected_workflow_version: int
expected_intent_version: int
from_state: str
to_state: str
trigger_id: str
class WorkflowStore:
def transition(self, request: TransitionRequest) -> WorkflowRecord:
"""Atomically compare expected state and append the next transition."""
raise NotImplementedError
The model does not mutate WorkflowRecord directly.
It can propose structured decisions.
The durable runtime validates and persists transitions.
Separate Orchestrator Decisions From Model Decisions
For example:
@dataclass(frozen=True)
class CandidateDecision:
candidate_id: str
action_type: str
evidence_refs: tuple[str, ...]
The model may return CandidateDecision.
Then deterministic workflow logic checks:
is intent current?
is candidate structurally valid?
is required verification complete?
is approval required?
is authority sufficient?
is state fresh?
Only then does the workflow transition.
Do Not Let the Model Invent Workflow State Names
Avoid designs where the model says:
{"next_state": "deployed_successfully"}
and the runtime trusts it.
State transitions should come from a finite protocol.
The model may provide evidence relevant to a transition.
The control plane decides whether that transition is legal.
Model Calls Are Activities With Budgets
Long-running workflows can accidentally spend enormous amounts of compute because each wake-up triggers fresh reasoning.
Track cumulative run budgets across the whole workflow.
model tokens
model calls
search expansions
tool calls
wall-clock active compute
external API cost
human-review requests
The workflow persisting for three days must not reset its cost budget every morning.
This extends Step 16’s budget ledger into durable time.
Budget Resets Must Be Explicit Policy
Sometimes a new intent version justifies a new budget.
Sometimes it should inherit the remaining budget.
Sometimes cancellation creates a separate reconciliation reserve.
Make that policy explicit.
Do not let restarts accidentally create free compute.
Durable Workflows Make “Agent Sessions” Less Important
A session is a UI concept.
A workflow is an operational concept.
One workflow may span:
- several conversations;
- several workers;
- several models;
- several days;
- several human reviewers.
Conversely, one chat session may create multiple workflows.
Do not use chat-session identity as the primary operational identity.
Coding Agent Example
Suppose an agent is implementing a large change.
workflow: upgrade authentication library
Possible sequence:
inspect repository
↓
create isolated workspace
↓
generate migration candidate
↓
run tests
↓
WAIT_FOR_CI
↓
CI passes
↓
request human approval
↓
WAIT_FOR_APPROVAL
↓
reviewer requests change
↓
reopen reasoning activity
↓
new candidate
↓
verify
↓
approval
↓
revalidate main branch
↓
merge / commit
↓
postcondition verification
This could take hours.
The model does not need to stay alive.
The workspace, evidence, intent, commitments and approvals do.
Research Agent Example
A research workflow may wait for:
- scheduled data release;
- document availability;
- human source approval;
- another specialist agent;
- market close;
- a benchmark result.
The durable state can preserve:
research question
claim graph
source provenance
pending evidence requests
current confidence state
verification gaps
When new evidence arrives, only affected claims need reevaluation.
The whole investigation does not need to restart.
Browser Agent Example
Suppose a browser agent is filling a complex government application.
It may need to:
collect documents
wait for user upload
resume form
wait for external verification
request confirmation
submit
verify submission ID
Session state may expire while waiting.
The durable workflow should preserve the logical process while treating browser session state as refreshable/non-portable operational state.
The workflow can resume with:
new browser session
fresh authoritative page state
same logical workflow
if compatibility permits.
DevOps Agent Example
A deployment workflow is a natural durable workflow.
prepare release
↓
reserve window
↓
WAIT_WINDOW
↓
check current intent
↓
check dependency health
↓
deploy canary
↓
WAIT_OBSERVATION_WINDOW
↓
evaluate SLOs
↓
promote or rollback
Trying to implement this as one LLM coroutine is unnecessary risk.
Durable Workflows Clarify Agent Boundaries
An important consequence is that the agent becomes less mystical.
You can ask:
What state is this workflow in?
What event is it waiting for?
What commitments exist?
Which transition is legal next?
Which reasoning decision is actually needed?
Instead of:
What is the agent thinking?
That is a much better operational model.
Failure Mode: Process Memory as Workflow State
Symptoms:
restart loses progress
waits disappear
retry counts reset
cancellation forgotten
Fix:
durable workflow state
Failure Mode: Conversation Transcript as State Machine
Symptoms:
model rereads history
infers where it thinks the process is
continues
This is fragile because natural language is ambiguous.
Fix:
structured durable state + transcript as provenance
Failure Mode: Sleeping Workers
Symptoms:
thousands of processes sleeping
while waiting for external events
Fix:
persist wait + release worker
Failure Mode: Retry Storm After Recovery
Symptoms:
provider returns
all waiting workflows wake
provider fails again
Fix:
backpressured reactivation
Failure Mode: Lost Cancellation
Symptoms:
worker never receives cancel signal
later commits stale work
Fix:
intent-version fencing at mutation boundary
Failure Mode: Old Approval Resumes New Candidate
Symptoms:
candidate changes while waiting
old approval reused
Fix:
approval bound to candidate/state/intent identity
Failure Mode: Workflow Completes With Live Commitments
Symptoms:
workflow says done
reservation / external obligation remains active
Fix:
terminal commitment invariants
Failure Mode: New Release Silently Changes Paused Workflow
Symptoms:
workflow sleeps under R17
wakes under R18
new semantics applied silently
Fix:
behavioral-release compatibility gate
Failure Mode: Replay Reissues Side Effects
Symptoms:
forensic replay sends email again
Fix:
activities recorded as historical results
workflow replay does not rerun effects
Failure Mode: Repeated Model Reasoning Burns Unlimited Budget
Symptoms:
every wake-up triggers expensive full reasoning
Fix:
durable cumulative budget ledger
+ decision-point-specific context
Failure Mode: Everything Becomes a Workflow
Symptoms:
simple 5-second task requires durable orchestration
Fix:
use ordinary code when ordinary code is enough
The series rule still applies:
Complexity must earn its place.
Failure Injection for Durable Agent Workflows
Test the workflow runtime deliberately.
Worker dies before activity starts
Expected:
activity safely retried
Worker dies after external effect but before recording result
Expected:
same operation identity
external reconciliation
no duplicate side effect
Human approval arrives after supersession
Expected:
approval rejected for stale intent
Timer fires twice
Expected:
idempotent wake-up
Provider recovers after large backlog
Expected:
controlled reactivation
not thundering herd
Behavioral release changes while workflow waits
Expected:
compatibility decision before resume
Commitment remains active when workflow tries to complete
Expected:
completion blocked or explicitly marked deferred/reconciliation
Workflow state write races with cancellation
Expected:
optimistic concurrency rejects stale transition
Duplicate external event arrives
Expected:
same event ID ignored after first consumption
Workflow sleeps past freshness budget
Expected:
revalidation before next consequential step
A Useful Workflow Transition Gate
The important checks can be centralized.
class WorkflowTransitionGate:
def validate(self, state, transition, context):
if not context.intent_is_current(state.intent_id, state.intent_version):
return "INTENT_SUPERSEDED"
if not context.workflow_version_compatible(state):
return "WORKFLOW_MIGRATION_REQUIRED"
if transition.requires_fresh_state and not context.state_is_fresh(transition):
return "REVALIDATION_REQUIRED"
if transition.requires_authority and not context.authority_allows(transition):
return "AUTHORITY_DENIED"
if transition.requires_verification and not context.verification_valid(transition):
return "VERIFICATION_REQUIRED"
return "ALLOWED"
Keep this deterministic.
The model can help produce candidates.
It should not decide whether stale intent is current.
Durable Workflow Outcomes Should Be Explicit
Useful terminal or suspended outcomes include:
COMPLETE
COMPLETE_WITH_DEFERRED_COMMITMENTS
FAILED
CANCELLED
SUPERSEDED
WAITING
HUMAN_REQUIRED
REVALIDATION_REQUIRED
RECONCILIATION_REQUIRED
WORKFLOW_MIGRATION_REQUIRED
POLICY_BLOCKED
Do not collapse all non-success states into generic failure.
Operationally, they mean very different things.
Benchmark Against Simpler Designs
Before adopting a durable workflow engine, compare it against:
single process
simple job queue
job queue + durable cancellation
coarse checkpoint/restart
Measure:
recovery correctness
side-effect duplication
orphan rate
operator burden
implementation complexity
latency
cost
If a simple queue solves the workload, use the simple queue.
The Deeper Architectural Shift
We started this series thinking mostly about better reasoning.
Now consider where the model sits:
durable workflow
│
┌──────────┴──────────┐
│ │
deterministic reasoning
state machine activity
│ │
└──────────┬──────────┘
│
verified action
The workflow survives:
- model replacement;
- worker replacement;
- provider replacement;
- process restart;
- human delay;
- temporary outage.
That is a much stronger architecture than asking one model invocation to somehow remain “the agent” for days.
The Principle
A production long-running agent should not be defined by the lifetime of a process or conversation.
It should be defined by a durable operational identity.
That identity includes:
intent
workflow state
commitments
external operation identities
waits
verification evidence
history
budgets
policy
Workers come and go.
Models come and go.
The workflow remains.
Make the workflow durable. Treat the model and worker as disposable.
That is how an agent survives for days without pretending that an LLM is a process manager, database, scheduler and distributed transaction coordinator.
And it leads directly to the next problem.
A durable workflow can survive partial failure.
But what happens when the system has already changed the world before a later step fails?
That is the subject of the next step:
transactions, compensation and reconciliation across external side effects.