Resume Is a State Matrix, Not a Path
Jul 27, 2026
When you add a way to re-enter a long-running workflow, the natural instinct is to design around the stopping point you can see. Usually that is the place where the process is visibly waiting for a human. But a workflow that pauses in one obvious place almost certainly also parks work in several less obvious ones — and a resume specified against the visible state will quietly strand everything else.
The problem
Long-running governed workflows — anything that moves an item through successive automated stages punctuated by human decision gates — accumulate a lot of legal intermediate states. An item can be:
- part-way through an automated stage, with no human input required yet
- sitting at a human decision gate, waiting for approval
- past a gate but not yet advanced into the next stage
- finished, or in a terminal handoff to another system
To an operator, only one of these looks like "paused". The gate is visible: something is explicitly waiting for you. The others look like nothing is happening, because nothing is happening — the process stopped when the session ended, and the item is sitting mid-stage with no prompt attached to it.
So when the requirement arrives as "let me pick up an existing item where I left off", the specification tends to get written as: find the pending gate, present it, take the decision. That is a path. It answers one question — how do I get back to the prompt? — and it silently answers a different question wrongly: what happens to every item that isn't at a prompt?
The failure mode is not a crash. It is a resume that appears to work, refuses or no-ops on a whole class of legitimately resumable items, and pushes the operator into manual recovery for reasons they can't easily see.
What actually happened
In a recent piece of work, the goal was to give operators a continuous experience across a sequence of human decision gates — a design gate, a technical gate, an acceptance gate — without creating a second authority over the workflow lifecycle. The interactive entry point was meant to present decisions and record them, while the existing controller remained the only thing allowed to advance stages.
The first specification handled the visible case well. If an item was waiting at a human gate, re-entry would find it, present the decision, write the outcome durably, and hand off cleanly.
What it did not handle was an item still legitimately in an earlier automated stage — work that had simply not reached a gate yet. The original spec narrowed re-entry so far that those items had no continuation route at all. They were valid, in-progress, and unreachable.
A specification audit caught this before implementation. Importantly, it was not flagged as wording drift or a documentation nit. It was flagged as product-intent drift: the described behaviour no longer matched what "resume an existing item" is supposed to mean to the person using it. The revision added exactly one delegation — hand the item to the existing component that already knows how to continue pre-gate automated work — and explicitly excluded the one thing re-entry must never do, which is start something new.
That single correction is the whole article. The implementation was otherwise sound. The interactive layer reused the existing gate writers rather than reimplementing them; it added the one missing writer for the gate that lacked one; that writer updated gate fields only and deliberately did not advance the stage, leaving that transition where it already lived. Acceptance stopped at a handoff rather than triggering downstream merge behaviour. The boundaries were disciplined. But without the audit, the feature would have shipped with a hole in its state coverage that no test would have failed, because no test would have been written for a state nobody had enumerated.
There was a second, smaller lesson from the same work. A pre-flight check bypassed an injected dependency and reached a real external tool during verification, which failed. The repair was to apply the injected function consistently. Verification then passed against the repaired head — which is worth noting only because it reinforces that the state of the current artefact, not the memory of an earlier run, is what you verify against.
The lesson
Re-entry is a contract over states, not a route to a destination.
If you specify resume as "get me back to where I was", you will implement it as a lookup. If you specify it as "for every legal state this item can be in, what happens when someone re-enters?", you will implement it as a dispatch — and you will notice the states you were about to drop, because you have to write a row for each of them.
The minimum matrix for a governed workflow looks something like this:
| State at re-entry | Expected continuation | | --- | --- | | Pre-gate, automated work outstanding | Delegate to the existing component that continues that work | | Waiting at a human decision gate | Present the decision, record it durably, stop | | Past a gate, awaiting stage advancement | Let the existing authority advance; do not advance from here | | Terminal / handed off | Report clearly; do nothing | | Not started | Refuse — re-entry is not initiation |
Every row is a decision. The rows you don't write become undefined behaviour, and undefined behaviour in a resume path shows up as silent no-ops in production, not as errors.
Note the last row especially. The most dangerous adjacent behaviour to resume is start. They look similar from the outside — both end with the system doing work on an item — and conflating them means a mistyped re-entry can initiate something instead of continuing it. Excluding initiation explicitly, in the specification rather than in the code comments, is cheap and prevents an expensive category of mistake.
The broader principle
There is a general shape here that goes beyond resume.
When you add a new entry point to an existing system, the specification tends to be written from the perspective of the person who wants the entry point, describing the situation that prompted them to ask for it. That situation is one state. The entry point, once it exists, will be used from all of them.
So the question to ask of any new entry point is not "does this handle my case?" but "what is the complete set of states from which this can be invoked, and what does it do in each?" That question is boring to answer and reliably surfaces gaps.
The related principle is about authority. A continuation surface should route and present. It should not become a second place where lifecycle decisions are made. In this work, the interactive layer was allowed to record gate outcomes but not to advance stages, because stage advancement already had an owner. That separation is what made it safe to add another way in at all — a new entry point into a system with a single lifecycle authority is a convenience; a new entry point that also decides things is a fork. (The general argument for keeping presentation layers out of lifecycle authority deserves its own treatment; here it is enough to note that the state matrix is only safe when authority is already settled.)
And finally: specification review earns its keep on exactly this class of problem. Coverage gaps in a state matrix are nearly invisible to code review, because the code that handles the states you listed looks correct — it is correct. Only a reviewer reading the spec against the intended user meaning of the word "resume" catches that a state is missing.
How to apply it
If you are about to add re-entry, continuation, retry, or "pick up where I left off" behaviour to a workflow:
- Enumerate the states before you design the mechanism. List every position an item can legally occupy, including the ones with no visible prompt attached. Do this from the state model, not from memory of how you use the system.
- Write a row per state. Each row names the state, the expected continuation, and the component that owns that continuation. If two rows name the same owner, good — that is reuse. If a row names a new component, ask why the existing owner cannot do it.
- Write the exclusions explicitly. State what re-entry must never do: never initiate, never advance a stage that another component owns, never trigger downstream irreversible actions as a side effect of recording a decision. Exclusions in a spec become tests; exclusions in someone's head become incidents.
- Delegate rather than reimplement. If a state already has a component that knows how to continue it, the re-entry path should call it once and get out of the way. Re-entry that reimplements continuation logic will drift from it.
- Review the spec against user meaning, not internal consistency. The useful question is: "if an operator says 'resume my item', which of these states do they expect to be covered?" An internally consistent spec that covers three of five states will pass code review and fail users.
- Test the matrix, not the happy path. One test per row. The rows that no-op or refuse need tests too — that is where silent failure lives.
None of this is expensive. Enumerating the states is an hour of work. The alternative is discovering the missing rows one support conversation at a time, months later, when the operator has already lost confidence that re-entry does what it says.