Picture the thing running your agent loop. Most people picture a long-lived orchestrator — a smart controller that holds the plan, tracks progress, and decides what happens next. That controller is the most fragile part of the design, and the best loops don't have one.

Thesis: A durable loop isn't driven by a persistent orchestrator holding state in its head. It's an append-only ledger of what's happened, plus a stateless scheduler that reads the ledger, dispatches the one iteration the current state calls for, writes the result, and forgets everything. Status is derived from the ledger, never stored. Continuation is emergent — the check's output is the next instruction. The payoff: a loop that's resumable, crash-safe, and safe to run twice.


This is Post 6 of "The Loop." Post 5 moved the loop's memory onto disk. This post shows that once you do, the disk state becomes the entire control system — and the "loop" nearly vanishes.

The Orchestrator Temptation

The instinctive architecture for a loop is a controller that stays alive for the whole run:

flowchart TD
    Orch["Long-lived orchestrator<br/>(holds plan + progress<br/>in its context)"]
    Orch -->|"dispatch"| W1["worker 1"]
    W1 -->|"result (into orchestrator context)"| Orch
    Orch -->|"dispatch"| W2["worker 2"]
    W2 -->|"result (context keeps growing)"| Orch
    Orch --> Rot["context rots · can't resume<br/>after crash · double-run corrupts"]

    style Orch fill:#9d4444,color:#fff
    style Rot fill:#9d4444,color:#fff

It's intuitive and it's a trap. The orchestrator holds the loop's state in its context, which means it inherits every problem from the last post: the state rots as the run grows. Worse, the state is only in that context — so if the process dies at iteration 40, the loop's memory dies with it. And you can't run two of them, or restart one, without corrupting the shared picture, because the source of truth is trapped inside a living process.

Invert It: Ledger + Stateless Scheduler

The durable design pulls the state out of the controller and onto disk, leaving the controller with nothing to hold — so it can be stateless, disposable, and dumb.

A framework I work with describes its driver as "a scheduler over the ledger, not a persistent parent: read state → pick the next actionable leaf → dispatch the cycle its status calls for → integrate → write → repeat." The scheduler is explicitly "a stateless role" that is "resumable and safe to run redundantly." All the state lives in the ledger; the scheduler holds none of it between steps.

flowchart LR
    Ledger[("Append-only ledger<br/>(the only source of truth)")]
    Sched["Stateless scheduler<br/>read → pick next → dispatch<br/>→ write → forget"]
    Ledger -->|"read state"| Sched
    Sched -->|"dispatch one iteration"| Work["Fresh worker<br/>(does one step)"]
    Work -->|"append result"| Ledger
    Sched -.->|"holds nothing;<br/>can crash / restart /<br/>run twice safely"| Sched

    style Ledger fill:#2d6a4f,color:#fff
    style Sched fill:#5a5a8a,color:#fff
    style Work fill:#5a5a8a,color:#fff

Notice what the scheduler is now: a loop so simple it's almost not there. Anthropic's reference patterns for long-running agents make the point viscerally — their driver is essentially a shell line, while grep -q '"passes": false'; do run-agent; done. The "orchestrator" is a grep over a results file. All the intelligence is in the workers and all the state is in the file; the loop itself is trivial by design. A trivial loop is a loop that can't corrupt its own state, because it doesn't have any.

Derive Status, Don't Store It

The subtle discipline that makes a ledger trustworthy: status is computed from the history, never written down as a fact.

It's tempting to store status: "in_progress" on a task. But a stored status can lie — it can disagree with what actually happened, drift out of sync after a crash, or get set optimistically by a worker that didn't finish. The robust pattern keeps the ledger append-only — a running story of events — and derives the status from that story on every read:

flowchart LR
    Story["Append-only story of events"] --> D{"derive status"}
    D --> S1["no 'built' act → open"]
    D --> S2["built, not validated → unverified"]
    D --> S3["validated PASS → verified"]
    D --> S4["validated FAIL → needs-rework"]

    style S3 fill:#2d6a4f,color:#fff
    style S4 fill:#9d4444,color:#fff

That same framework's spec is blunt about it: "status is derived from each deliverable's append-only story, never stored: open → build → unverified → review → verified | needs-rework." Because status is a function of the events, it can't drift from them. A crash mid-iteration leaves a coherent ledger — the interrupted work simply reads as "not yet done" because the completing event was never appended. There's no half-written status to reconcile.

Continuation Is Emergent

Here's the part that reframes what a "loop" even is. In the cleanest versions, there is no loop command at all.

One goal-driven system I've used treats this as an invariant: "continuation is emergent, not prescribed... the validation response is the instruction to continue. No separate 'loop' command exists in the user contract." The user says "build X until it meets Y." The user never says "iterate five times" or "run the loop." What actually happens: each iteration runs a check, the check emits a validation.json, and its next_steps[0] becomes the next iteration's task. The loop isn't a control structure someone wrote — it's the emergent consequence of measuring against a bar and feeding the result forward.

flowchart TD
    Goal["User: 'build X until it meets Y'<br/>(a goal + a finish line —<br/>never 'iterate N times')"] --> Run["Iteration does work"]
    Run --> Val["Validation → next_steps[]"]
    Val -->|"gap remains → next_steps[0]<br/>becomes the next task"| Run
    Val -->|"bar met → next_steps empty"| Stop["Loop ends<br/>(nothing left to do)"]

    style Goal fill:#2d6a4f,color:#fff
    style Stop fill:#2d6a4f,color:#fff
    style Val fill:#5a5a8a,color:#fff

This is the whole what/why-vs-how thesis collapsed into a control structure. The user supplies the what (X) and the bar (Y). The loop's existence, length, and steps — the how — are emergent, not authored. Termination isn't a separate concern bolted on; it's just "next_steps is empty," which is just "the gap from Post 3 closed." Stop conditions, continuation, and iteration count all fall out of one thing: the check's output feeding the next step.

What the Inversion Buys

Moving from orchestrator to ledger-plus-scheduler isn't stylistic. It buys concrete operational properties:

Property Orchestrator (state in context) Ledger + stateless scheduler
Resume after crash Lost — state died with the process Read the ledger, continue
Run two instances Corrupts shared picture Safe — ledger is the sole truth
Re-run redundantly Double-applies effects Idempotent — derived status dedupes
Audit what happened Buried in a transcript The ledger is the audit log
Context rot over a long run Accumulates and degrades None — scheduler holds nothing
Swap the model mid-run Risky — context assumes continuity Trivial — each iteration inits blind

The through-line: because nothing durable lives in a running process, nothing durable dies with it.

When an Orchestrator Is Fine

A stateful controller isn't always wrong:

  • Short, in-process loops. A three-step loop inside one function doesn't need an append-only ledger — a local variable is fine. The apparatus is for loops that outlive a process.
  • Truly serial, uninterruptible work. If the loop can't be resumed or parallelized anyway (a single hardware device, a stateful external session), the ledger's resumability buys less.
  • Latency-critical inner loops. Reading and deriving status from disk every iteration has a cost. A tight, fast loop may want state in memory — just know you've traded away resumability for speed.

The ledger pattern earns out exactly when loops are long, unattended, and expensive to restart from scratch.

What to Do Next

  • Find where your loop's state lives. If the answer is "in the orchestrator's context," it dies when that process does. Move it to an append-only artifact on disk.
  • Make the driver stateless. The thing running the loop should hold nothing between iterations — read the ledger, dispatch one step, write, forget. Aim for a driver simple enough to be a shell line.
  • Derive status; never store it. Compute each item's state from its event history so it can't drift. A crash should leave a coherent ledger, not a half-written flag.
  • Let continuation emerge. Don't write a loop that counts. Write a check that emits the next step, and let "no next step" be your termination. The loop should be a consequence, not a construct.
  • Test resumability by killing it. Kill the process mid-run and restart. If it can't pick up from the ledger, your state isn't really externalized yet.

Next, and last: Recursion That Can't Run Away — when an iteration's next step is itself a whole loop, and the floor that keeps the nesting finite. Related: Bound, Don't Script and The Files Are the Memory.