My Agent Kept Skipping Steps. The Fix Was Taking the Order Away From It.
My agent was useful for summarizing email threads, condensing notes and analyzing documents. I then pushed it at a repeatable dashboard that pulled from a dozen changing sources: email, Slack, Teams chats, SharePoint, presentation decks and meeting transcripts. Those sources did not carry equal weight or trust, so the agent had to judge what mattered before assembling the result. As the job grew, I stopped trusting it to carry out every required step.
The tell was specific, and if you’ve done this you’ve seen it too: the agent comes back with “ah, I missed that step,” or I catch myself asking “did you actually do that?” and find out it hadn’t. Not every run, and not the same step twice. I’d stopped being able to trust a repeatable job to run the same, correct way each time.
I didn’t want to give up the autonomy. That’s the good part, and forcing every task down a rigid script would throw it away. What I wanted was a structure I control and can build up progressively: guardrails I dictate where the steps matter, and a free run for the agent where they don’t. A contract between me and my agents. And it should decide which mode it’s in by reading the intent behind the ask: is this a repeatable, predictable job — a dashboard, a report, a daily brief, something I can compare against what it produced last time — or open-ended research with no past to check it against and maybe no repeat in its future. The first kind wants rails. The second wants to be left alone.
This became the applied version of an argument I had made earlier in Most Teams Reach for Fine-Tuning Too Early: when the model has the capability but its behavior is unreliable, the next lever is often the harness rather than the weights.
One update before the original build notes: the ordering fix held. The broader safety claims did not. When I later put the harness into recurring work, I learned that a plain lock and a restartable-looking state file did not make overlapping or interrupted runs safe. The second draft and Ratchet Runtime are the corrected design. I am keeping this article as the record of the first build because the central result is still useful: declared steps stopped being skipped.
The failure a better prompt couldn’t fix
My first instinct for the repeatable kind was to fix it with prompting. I wrote the steps out more explicitly, numbered them, added “do not skip any step,” put the important one in bold near the top. It got a little better and stayed unreliable. Eventually I stopped and asked why a firmer prompt wasn’t landing. The problem was structural.
The written checklist was still an instruction to the model, with no separate mechanism proving that each step had happened. My runs showed the omission, but they did not isolate why the model skipped a particular step. Longer instructions were not a dependable fix in those tests. I needed the next step to depend on a recorded, checked output rather than on the agent remembering where it was in the plan.
Code owns the order; the agent owns the work
The harness moves the plan into a file and lets deterministic code decide which step is ready. That machinery only switches on for the repeatable asks. When the intent reads as open-ended, the harness routes it to a free run; exploration does not need the same declared sequence as a dashboard refresh.
Concretely, a workflow is a graph of steps written as data. Each step, or node, declares what it produces and what it consumes. A small Python helper reads that graph, works out which nodes are ready, and hands the agent one node at a time. The agent still does the real work inside a node (the judgment, the tool calls, the analysis) but it never gets to decide the sequence. In the dashboard workflow I’ve been testing it on, this is what stops a declared step from being skipped: publish consumes analyze.findings. That output doesn’t become available downstream until the analyze node has recorded it, passed its verify, and cleared its review gate. So publish cannot become “ready” until analyze is done — not because the prompt politely asked for that order, but because the data it needs isn’t available yet. The ordering falls out of what each step needs from the step before it, and it’s enforced by code that can’t be talked out of it.
The build under all this is deliberately plain. It’s two pieces that each stay in their lane. One is a small Python helper, under a thousand lines, that owns everything deterministic: it validates the graph, works out what’s ready, runs each verify, records what happened, and keeps the run’s state on disk. The other is the agent, which does the actual work inside a node — reading the sources, reasoning, writing the output — and nothing else. The helper never reasons, and the agent never decides the order. Every command prints a single result and exits hard on any error, so a crash in the machinery can’t quietly become a skipped step. The exact failure I was trying to kill doesn’t get to sneak back in through the plumbing.
This abridged trace comes from an end-to-end dashboard test. I’ve shortened the full Python invocation to anchor and the helper’s JSON to the fields that matter. The underlying commands and state transitions are real.
$ anchor start dashboard-refresh --param quarter=Q2
run: <run> · audit: full
$ anchor ready <run>
ready: [fetch_sf, fetch_slack] # no deps -> both run in parallel
$ anchor record <run> --node fetch_sf --output out/fetch_sf.json --meta '{"row_count": 10}'
state: recorded # output is quarantined, not done
$ anchor verify <run> --node fetch_sf
pass: true · state: done # verified output is promoted
$ anchor record <run> --node fetch_slack --output out/fetch_slack.json
$ anchor verify <run> --node fetch_slack
pass: true · state: done
$ anchor ready <run>
ready: [reconcile] # fan-in: waited for BOTH fetches
$ anchor record <run> --node reconcile --output out/reconcile.json --meta '{"row_count": 42}'
$ anchor verify <run> --node reconcile
pass: true · state: done
$ anchor record <run> --node analyze --output out/findings.md --meta '{"metrics": {"anomaly_count": 0}}'
$ anchor verify <run> --node analyze
delegate: subagent · criterion: each claim cites a source row
$ anchor verify <run> --node analyze --result pass
state: awaiting_gate # verified, still quarantined
$ anchor gate <run> --node analyze --approve
state: done # approval promotes the findings
$ anchor ready <run>
ready: [publish] # alert SKIPPED (anomaly_count = 0)
gates: publish = choose(slack | email | both) # pauses for me
Parallel fetches, a fan-in that waits for both, a verify that must pass before the run advances, an alert node that skips itself when there’s no anomaly, and a gate that halts and asks me which channel — every one of those decided by the helper reading the graph, not by the model remembering to.
What counts as verified
A step can record an output without releasing it downstream. Its verify must pass first. That makes the difference between an artifact the agent says it produced and one that has cleared the declared check. The agent still has room to choose its method inside a node, from a tightly specified data pull to an open analysis.
The decision I went back and forth on most was how a step proves it’s done. The easy version is to let the agent check its own work, but a step the model grades itself on isn’t really checked; that’s the same trust problem one layer down. So a verify is a small closed expression the helper evaluates, not the agent: row_count > 0, exists(analyze.findings), a named metric crossing a threshold — all mechanical. Anything that needs judgment, like “does every claim in this analysis cite a source row?”, I hand to a separate check the agent runs and label as judgment rather than fact. The tradeoff is real and I took it on purpose: that little language can only state simple, mechanical truths. I’d rather have a check that’s dull but trustworthy than a clever one I can’t trust.
That mechanical check is only as strong as its inputs. The helper can establish that an artifact exists and read recorded metadata, but version 0.3.1 does not independently derive something like row_count from every possible file format. For a metric I need to trust deeply, the next step is a verifier that computes it from the artifact rather than accepting the node’s metadata.
Why I kept it small
The rest is a run of deliberate subtractions. It works on flat files, YAML for the plan and JSON for state, with a plain lockfile that reduced accidental overlap in the supervised scratch runs I was doing, and no database, server, or queue anywhere. I later learned that the lock was not enough to make concurrency or restart safe. The flat files still bought me something I valued: I could open or hand-fix a run in a text editor. They also came with limits I had not yet tested well: it would not scale out, survive the machine dying mid-run, or coordinate a fleet. Two dials kept it cheap when it needed to be: how much of each run’s audit trail to keep, and a node-and-time budget that halted unattended runs before they ran away.
Those files can contain sensitive material, so credentials stay in the assistant’s connected tools rather than in a workflow, and the audit level can be turned down when a run should not retain source snapshots.
None of this is because the existing tools fall short. I love LangChain and have leaned on it heavily. LangGraph is the closer structural comparison, with persistence, durable execution, and human-in-the-loop control, and Microsoft’s Conductor takes the declared-YAML pattern further into multi-agent execution. Both can be used by one person; my choice was about surface area, not capability. I wanted one agent, my own knowledge work, flat files I could inspect, and something I could grow one workflow at a time. For that need, a small harness I fully own fit better than a framework whose production capabilities I would use only a fraction of. Conductor did settle one thing for me, though: another team had landed on the same pattern — a declared plan as data, deterministic routing, checkpoints for a human — which says the shape is right far more convincingly than my own conviction could.
Once it existed, packaging mattered as much as the engine. I registered it in my assistant as a plugin, so any project I’m in can init it in one shot: full autonomy where the work is open, hard rails where the steps matter, no files to copy around, the engine shared while each project keeps its own workflows and runs. I’ve put the whole thing up as an installable Claude plugin — github.com/pr9868/anchor-harness — the skill, the helper, the five starter templates, and the full design spec, enough to install it, point it at your own workflows, and read the pattern in working code. I built it inside my own assistant, but nothing about the shape is tied to that, so take it as-is or adapt it for whatever AI tool you use.
Environment
I built and run it inside my assistant on a Mac. The plugin installs into Claude; the deterministic half is a single Python script whose only dependency is PyYAML. No server, no database, no second machine.
Scroll sideways for more columns.
| Runtime | Claude, installed as a plugin |
| Machine | macOS |
| Helper | Python 3 · one dependency (PyYAML) |
| Storage | Flat files: workflows, run state, and audit trail on disk |
| Tested | Scratch projects, end to end; not yet under real load |
The pattern, and where it stands
This was version 0.3.1 and single-user, and the trickier operations were still only half in code: merging disagreeing sources, fanning a step out over a list, retrying on failure. The helper validated and surfaced those, but the agent carried them out. I had run it only in scratch projects, nothing past a handful of nodes, so I did not yet know where it would strain. What this build established was narrower: the agent stopped skipping declared steps because the plan moved out of its context and deterministic code decided what came next. It did not establish safe concurrency, recovery, or durable execution.
I later put that design into recurring work and found its next boundary: deterministic order did not make an overlapping or interrupted run safe to restart. The second draft is what I learned when I stopped treating order as most of the safety boundary.
Disclaimer: The views and opinions expressed in this account are those of my own and do not represent those of my employer, NVIDIA.