← Projects

My Agent Kept Skipping Steps. The Fix Was Taking the Order Away From It.

Over the last two years I’ve watched AI agents climb a ladder. First a chat-based assistant. Then a chat that could look across data from different sources instead of just what I pasted in. Then agentic workflows that use tools, take actions, and reason at runtime rather than following a fixed script. I’ve enjoyed the climb, and the simplicity it brought: at the reasoning tier the agent started to feel like a capable intern sitting beside me, one that’s willing and able to think, not just fetch.

Where it paid off first was the small repetitive stuff that eats a day: summarize a long email thread, condense my own notes, analyze a document. Easy wins, real time saved. So I pushed it at the heavier work I wanted off my plate — building a dashboard that pulls from a dozen places that never stop changing: email, Slack, Teams chats, SharePoint, presentation decks, meeting transcripts. Those sources don’t carry equal weight or equal trust, so the agent can’t just concatenate them. It has to reason about what matters before it assembles anything, and the context it has to hold gets big. That’s where I started to notice the reasoning and the plan getting less reliable and less deterministic than I wanted them to be.

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.

An LLM doesn’t execute a checklist. It predicts the next token. Give it a long instruction with twelve steps, and after step three, if “summarize the work and report success” is a natural-sounding continuation, that’s what it generates, with steps four through twelve still sitting right there in the prompt. A longer context doesn’t mean the model uses every instruction equally, either: information buried in the middle is often used least reliably, and a run’s accumulating tool output can push the original plan into that weak zone. And it compounds: if, purely for illustration, each step independently succeeded 95% of the time, a ten-step task would finish correctly about 60% of the time, and a twenty-step task about a third. Multi-step agent work is unreliable by multiplication. On top of all that, the plan only ever lives in the model’s context, so it decays as the run gets longer, and nothing anywhere proves a step happened. The model can believe it reconciled the data when it didn’t.

Code owns the order; the agent owns the work

Once it’s framed that way, the fix stops being “write a firmer prompt” and becomes “stop keeping the plan in the one place that decays.” So that’s what the harness does. It moves the plan out of the model’s head and into a file, and it lets plain deterministic code — not the model — decide what runs next. That machinery only switches on for the repeatable asks. When the intent reads as open-ended, the harness routes it to a free run and stays out of the way; putting exploration on rails would just add ceremony.

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.

The Anchor architecture. A request from you first hits a dispatcher that picks a lane: rails for a known workflow, assisted for something novel it drafts first, or free for open-ended work it leaves alone. For rails and assisted, the plan is a DAG written in YAML that lists each node and what it produces and consumes. The engine is a split: the orchestrator is the agent that does each node's work at its declared autonomy (locked, guided, or open), and a deterministic Python helper decides the order, runs each verify, records what happened, and enforces a run budget. The helper hands the agent the next ready node; the agent records and verifies back. At marked steps a gate pauses for you to approve, choose, or review. Everything durable lands in flat project files: runs with state and an audit trail, deliverables, and a cache.
The split is the design: a deterministic helper decides order, readiness, and whether each step verified; the agent does the judgment work inside whichever node it's handed. The plan lives outside both, as a DAG in YAML. Gates hand control back to me; everything durable lands in flat files.

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

Two things keep it usable rather than just rigid. First, each step carries a verify the code runs — something mechanical like row_count > 0 or exists(analyze.findings) — and the run doesn’t advance past a node until it passes. A recorded output stays quarantined; only a successful verify marks the node done and makes its output available downstream. That’s the part that closes “the model believes it did something it didn’t”: belief isn’t enough, the output has to exist and clear a check. Second, the determinism is only in the sequencing. Inside a node I can still dial the autonomy up or down: tell it to follow instructions to the letter for a mechanical data pull, or let it design its own approach for an open analysis. I keep the agent’s judgment; I just fence it inside steps whose order and existence I control.

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.

RuntimeClaude, installed as a plugin
MachinemacOS
HelperPython 3 · one dependency (PyYAML)
StorageFlat files: workflows, run state, and audit trail on disk
TestedScratch projects, end to end; not yet under real load

The agent side is the one part tied to my setup: I built it on Claude, so the skill that teaches the agent the protocol is written for it. The helper underneath is plain Python and knows nothing about which assistant is driving it, which is what keeps the pattern portable.

The pattern, and where it stands

None of the fix is specific to my setup. Getting the plan out of the model’s context and into data the system re-reads did the work, along with letting deterministic code decide the order so a declared dependency can’t be skipped without the run failing, and proving each step with a check the code runs rather than one the model grades itself on. Human gates and an audit trail sit on top, and open-ended work stays off the rails. The shape holds whether you build it on flat files like I did, in LangGraph, or on something like Conductor.

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.

← All projects