Picking a run up
A loop that runs for a week will be stopped and started: a machine goes down, somebody presses by hand, or a turn takes the process with it. A flow is a file on disk, and the weaver who wrote it may say it can be picked up where its last run left off.
Try it
Run a resumable flow twice with the same command:
hmz exec -f nightly -a claude/claude-opus-5:high "keep the tests green"
hmz exec -f nightly -a claude/claude-opus-5:high "keep the tests green" # round 2, not round 1The second run finds what the first left behind, and carries on from round 2. In the interface, /resume is that second line typed at the prompt.
Saying so
For the weaver. To make a flow resumable, give it resumable=True and a dict. The dict is where the flow keeps what the loop itself knows: which round it is on, which files it has been through, what it has decided so far. It is not a second copy of the transcript — the backends keep that, and the run's epic already says which sessions it opened.
"""A Ralph loop that knows which round it is on."""
from typing import Any
from hmz.flows import Agent, flow
@flow(resumable=True)
def run(agents: tuple[Agent], task: str, state: dict[str, Any]) -> None:
(agent,) = agents
while True:
state["round"] = state.get("round", 0) + 1 # writing it into the state saves it
agent(f"{task}\n\nRound {state['round']}.", suppress=True)The dict is the flow's last argument, after the config for a flow that takes one. A flow that says resumable=True but declares no such argument says so at the first call, before a turn has been taken, rather than starting over in silence. A flow that says nothing runs from the top every time. See Writing a flow.
Where it lives
What a flow keeps goes in the epic of the run that wrote it, as state.json:

The state is keyed by flow, so a flow that calls another one is two flows, each keeping its own state side by side in the one file and neither writing the other's:
{"nightly": {"round": 12}, "rlar": {"seen": ["src/pay.py"]}}The key is the name the flow was run under, so -f nightly and the path to that same file are two names and two states.
When it is saved
As the flow writes it. Setting a key, removing one, or calling update or setdefault writes the file again. A run worth picking up is one that was stopped or killed, and state written only at the end is state such a run has none of.
Writing inside a value the state holds is a change no mapping can see: appending to a list, or writing into a dict of its own, is saved when the run ends.
Keep to what JSON holds. Anything else is written as its str, so a Path put in comes back out a string. A value that cannot be written at all leaves the last save standing rather than ending the run: a loop that died for want of writing down where it got to would be worse than one carrying on from a round ago.
Running it again
There is no flag for it: running the flow again is what picks it up, as in Try it above. It carries on from the last run of that flow in this directory that left anything — a run that wrote nothing is skipped for the one before it. Runs are kept under the workspace they ran in, so another checkout carries on from its own last run there.
It is found by what the state holds rather than by what the run was of — which is what lets a flow that was called by another be picked up too, under its own name.
Picking one up from the interface
/resume carries the last run in this directory on: that run's own flow, on its own agents, with what it was asked to do, and on what it left behind. Nothing is named, because there is nothing to name — the last run is the last run. Which one that was comes back on the line that starts it —
carrying on from 20260910T021407.882Z-a3f19c: nightly on what that run left behind— because the person typing it has usually been away, and which day's work resumed is the thing they need to know first.
It differs from typing the line again in the two ways the prompt makes it differ. It is the last run here, whatever flow that was, rather than the last run of a flow you named. And it is that run and no other: where the last run is not one to carry on it says which reason that is rather than reaching past it for an older one, since a loop carried on from the day before yesterday, because yesterday's died before it wrote anything down, is a day's work thrown away in silence.
no flow has been run here | Nothing has ever run in this directory. |
<run> cannot be read back | Its record is not one: a run that died mid-line left a line rather than an epic. |
<flow> does not say it can be picked up | Asked of the flow as it stands today, not of what the run recorded. |
<run> left nothing behind | It stopped before it wrote down where it had got to, or it emptied what it wrote — which is the flow saying the next run starts clean. Say what to do and it starts from the top. |
no picking a run up while a flow is running | A run picked up is a flow started, and one is going. ctrl+c twice or /stop stops it first. |
no picking a run up while the flow is still stopping | ctrl+c twice was pressed and the flow has not gone yet — it is closing out the turn it was in, and still writing down where it got to. |
/resume takes nothing after it: a line that names a run is said back rather than dropped. To carry on a run that is not the last one, open the list and go into that run — which is the next section.
Carrying an older one on
/epics is every run of a flow in this directory, newest first: when it happened, which flow it was, what it was asked to do, how many sessions it opened, and a mark on the runs whose flow says it can be picked up. Enter goes into the run under the cursor — which says where that run is written down, and offers what there is to do with it:

| resume this run | Run the flow again on what this run left behind |
| export it | The whole run as one archive, its trace and its session logs in it — see Exporting a run |
It is /resume with the run already named. The same three reasons a run cannot be carried on are said here in the same words, so the table above holds inside a run as well as at the prompt.
The mark in the list and that first row are one question, asked of the flow rather than of the run. The weaver may have rewritten it since, so what can happen next is what it says today:
- A flow marked
resumable=Trueafter a run of it has that older run marked and offered too. Taking the row on one of those saysleft nothing behind: it ran while the flow still said nothing, so there is no state to pick up and nothing to carry on from. - A flow that has since dropped the mark has neither the mark nor the row, whatever the run wrote down at the time; where the row is gone, the reason stands under the list.
- A flow that will not load at all reads as one that says no — a flow that cannot be read cannot be run.
Exporting is offered for every run, whatever its flow says.
Carrying one on is refused while a flow is running, on the sheet rather than on the way out; stopping is what stops a flow.
Naming a run rather than taking the last one is the same thing from Python:
from hmz.runtime.runner import Runner
Runner("nightly", agents, resume=at).run("keep the tests green")at is that run's own directory: the path drawn at the top once you are inside the run, and what hmz.runtime.epic.epics() lists.
What carrying on runs
The flow, its agents and what it was asked to do all come off the run rather than off whatever the interface happens to be set up on — an agent swapped under it would be a different run wearing its name. Each agent starts at the backend, model, effort, permission and account the run wrote down.
Two things are not the run's. The person at the prompt is not an agent anybody chose, so a flow that talks to one is handed a fresh one. How the flow was set up is not written in the run, so that is what this project remembers for the flow now.
An epic is never reopened
What carries on is written into an epic of its own, and its began line says which run it was picked_up from. The run being picked up is read and left exactly as it was, so carrying the same run on twice is two runs from one starting point rather than one record with two runs inside it.
A week of stops and starts therefore reads as a run per stretch — its own sessions, its own trace, its own end — rather than one enormous epic claiming to have begun on Monday.
An atlas picks itself up
Everything above is a flow keeping what it wants to carry by hand. An atlas does not have to: its body is compiled into a graph, every node's answer is written down as it arrives, and picking a run up is walking that graph over the answers it already has until it reaches the node that has none.
See also
- Tracing — what else a run writes down, and reading one back
- Stopping — what makes a run worth picking up
- Flows › A flow that can be picked up
- TUI › Commands