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.
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, and the answer is structural enough that it’s worth being precise about.
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, and it’s worth spelling out because a few of the decisions are why it works at all. 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. One rule in the helper matters more than it looks: 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.
Here’s an abridged trace 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 so two runs can’t collide, and no database, server, or queue anywhere. That buys me something I value, being able to open or hand-fix any run in a text editor, and costs me things I don’t need as one person: it won’t scale out, won’t survive the machine dying mid-run, won’t coordinate a fleet. For a personal harness that’s a good trade, and the day it stops being one, the honest move is to move up to a real engine rather than bolt scale onto this. Two dials keep it cheap when it needs to be: how much of each run’s audit trail to keep, and a node-and-time budget that halts unattended runs before they run 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.
| 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 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
If there’s one thing to take from this without my code, it’s that none of the fix is specific to my setup. What did the work was getting the plan out of the model’s context and into data the system re-reads, 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.
I’ll say plainly where this stands. It’s version 0.3.1 and single-user, and the trickier operations are still only half in code: merging disagreeing sources, fanning a step out over a list, retrying on failure. Right now the helper validates and surfaces those, but the agent is what carries them out — exactly the kind of seam I want to pull back into deterministic code as it matures. I’ve run it only in scratch projects, nothing past a handful of nodes, so I don’t yet know where it strains — whether the verify conditions get brittle at twenty nodes or the graph just gets annoying to author. It’s a genuine work in progress; over the next few weeks I plan to put it through my own live workflows, find where it breaks, and refine it, and write up what I learn. But the core bet has already worked for me: the reason my agent stopped skipping steps isn’t that I finally found the magic wording. It’s that I stopped asking it to hold the plan in its head, and let something that can’t forget decide what comes next.
Disclaimer: The views and opinions expressed in this account are those of my own and do not represent those of my employer, NVIDIA. I may have gotten something wrong or missed a detail, and I’m always happy to be corrected.