MODULE 04 OF 12 · RELEASE 1
State, Memory & Durable Execution
Work that remembers where it got to, even if the power goes out mid-task.
You are following a recipe with twelve steps. Halfway through, the phone rings and you leave the kitchen for an hour. When you come back, you look at the pan and the ticked-off steps and carry on. You do not start again from the shopping.
So what is this really about?
Long jobs in software need the same thing: a written note of what has already been done. If the program restarts — a crash, a deploy, a server moving — it should read the note and carry on, not start over. Starting over is not just slow; if step two sent an email, starting over sends it twice.
QUICK CHECK
A four-step job crashes after step two. Step two sent a confirmation email. What must resuming NOT do?
The words you will hear
- State
- The ticked-off steps. What has happened so far, written down where it survives a restart.
- Durable
- Survives the program stopping. In a database, not in memory.
- Resume
- Picking up at step seven instead of step one.
- Memory (conversation)
- What was said earlier in a chat. Useful context — but not the official record of what was done.
By the end you will be able to
- Write a long job down as steps rather than holding it in memory.
- Resume without repeating anything that already happened outside your system.
- Keep chat memory separate from the official record.
Going deeper
Why in-memory work disappears
A long job written the obvious way holds everything in variables: which step it is on, what each step produced, what to do next. The call stack is the plan.
That works right up until the process stops. And processes stop constantly for entirely routine reasons — you deployed, the platform moved your container, a memory limit was hit, someone restarted something. None of these is an emergency. All of them lose every job that was in flight.
The fix is not to prevent restarts. It is to make them uninteresting.
Write the plan down
Instead of holding the job in memory, keep a row that describes it:
{
id,
task, // what we are doing
step, // which step we are on
results, // what each finished step produced
state, // running | waiting | done | failed
updated_at,
}Now a restart is a read. Load the row, look at step, carry on. Nothing is inferred from memory because nothing important was in memory.
This also makes the job inspectable while it is running, which is worth as much as the recovery. You can answer "where is this one stuck?" with a query rather than by adding logging and waiting for it to happen again.
Record before you act, not after
This is the rule the whole module turns on, and it is easy to get backwards.
The wrong order: do the thing, then record that you did it. If the process dies in between, the thing happened and nothing knows. Resuming does it again — a second email, a second charge, a second order.
The right order for anything irreversible: record that you are about to do it, do it, then record that it finished. A crash in the middle leaves a record saying "this was started and we do not know if it completed", which is awkward but honest — and far better than silently doing it twice.
For steps that only touch your own database, you can be simpler: do the work and the state update in one transaction, so either both happen or neither does.
Resuming is not replaying
There is a tempting alternative: instead of recording each result, just run the job from the start again and skip the steps that already happened.
It falls apart quickly. Skipping requires knowing what happened, which requires the record you were trying to avoid. And any step involving a model produces a different answer on the second run, so "replay" quietly becomes "do something different", which is worse than either alternative.
Store the result of each step. Resuming reads results; it does not recompute them.
Two kinds of memory, and why mixing them hurts
"Memory" in an agent usually means one of two very different things, and treating them as one causes real bugs.
Workflow state is what has happened: steps completed, results produced, actions taken. It is the source of truth. It must be exact, and it must never be summarised or dropped.
Conversation memory is what was said: the exchange so far, used to give the model context. It is helpful, it is inherently lossy, and it has to be bounded because conversations grow forever while context windows do not.
The bug that comes from conflating them: a system summarises old conversation turns to save space, and the summary quietly drops the fact that a refund was issued. Now the model believes no refund happened. If the refund had been workflow state, it would still be exactly recorded, because workflow state is never summarised.
Keep them in separate places with separate rules. Conversation memory needs a stated eviction policy — a turn limit, a token budget, a summarisation point — decided deliberately rather than discovered when a context window overflows in production.
Stopping properly
Every run needs a way to end that is not success. A step that has failed five times should move to failed and stay there, with the reason attached.
Without a terminal failure state, a job that cannot succeed retries forever: burning money, filling logs, and hiding the actual problem inside noise. A run that has given up and said why is far more useful than one still optimistically trying.
What good looks like
You can kill the process at any point during a job and restart it, and the job finishes correctly without repeating anything a customer would notice. You can query where any run has got to. And conversation memory has a size limit you chose, rather than one you will discover.
How it is put together
A run record holds the task, its current step, and the result of every completed step. Each step is a pure function of that state plus its own side effect, recorded before the step advances. Resuming means reading the record and continuing, not replaying. Conversation memory is a separate, size-bounded store — it informs prompts and is never the source of truth for what has happened.
Where you start
A three-step agent flow held entirely in memory, which loses everything if the process restarts.
The lab
- Model the flow as a run record with an explicit step and a result per step.
- Record each side effect before advancing, so a crash cannot lose it.
- Add resume: given a run id, continue from the recorded step.
- Add a bounded conversation memory with an explicit eviction rule.
- Add a terminal failure state that stops the run rather than looping.
What goes wrong, on purpose
The scenario. The process is restarted between step two and step three.
What should happen. Resuming continues at step three. Steps one and two are not repeated, and their side effects occurred exactly once.
How you prove it works
- A test interrupting a run and asserting resumption at the correct step.
- A test asserting completed side effects are not repeated on resume.
- A test asserting conversation memory stays within its bound under a long exchange.
What you walk away with
An agent flow you can kill at any point and restart without duplicate effects, and a memory store with a stated eviction policy.
