Designing agentic development workflows: principles for a workflow you can trust

The machinery, and the doctrine that governs what a step does with its own judgment.

A workflow’s phases and its cast are the easy half. The hard half is keeping that order honest when every step is carried out by something that would rather be agreeable than correct.

What follows are the principles that hold that order in place, in two groups. The machinery comes first, then the doctrine that governs what a step does with its own judgment. Almost every one of them exists because something went wrong in a way that produced no error.

This part assumes part one’s vocabulary: orchestrator, skill, agent, channel adapter, deterministic primitive, and the run directory. Part one introduces all six.

Machinery Principles

Where state lives, how one phase hands work to the next, what a gate has to be to count as one, and who is allowed to decide that a loop has finished.

1. The orchestrator is thin, because only a sub-agent can carry a model

In current agent runtimes, the model and reasoning effort serving a step can only be set when that step is dispatched as a sub-agent. A skill’s own metadata does not change what serves it.

That single platform fact drives the architecture. The orchestrator runs on whatever model the session happens to use, so it stays cheap and mechanical (control flow, gates, dispatch, wiring), and every act of judgment is pushed into an agent that declares its own tier. When a design wants “this decision deserves the strongest model and maximum effort”, the only way to express that is to make the decision a dispatched agent.

Generalized: find the unit your runtime lets you configure, and make that unit the boundary of judgment.

2. The run directory is the only channel

Every run gets a durable directory, and that directory is the only channel across a dispatch boundary. Inline steps share the orchestrator’s context and could pass things in memory; a dispatched sub-agent cannot. Design for the boundary and the inline case comes free.

The reason is mechanical: environment variables and the working directory do not survive a call into a sub-agent, so the run directory’s path is passed as a literal argument into every dispatch, and anything a later phase needs must be on disk. Run state lives in one small JSON file, written atomically (write a temp file, then rename) so a crash cannot leave a half-written record.

That pays off twice. A run becomes inspectable by anyone, including a human with no access to the transcript. And it becomes resumable: if the state file carries a phase cursor, re-entering the workflow continues from where it stopped instead of restarting over a human’s earlier approvals.

3. Artifacts are the interface between phases

Each phase produces a named file and the next phase consumes it: the request, the exploration, the plan, the tests, the implementation notes, the validation record, the report.

One small detail worth stealing: when a phase transforms a file, give the output a different name from the input. A step that writes its result over its own input will, on a second run, read its own output, and any deduplication or comparison it was doing silently stops working.

Because the interface is a file, a phase can be re-run, inspected, replaced by a human doing it manually, or swapped for a different implementation without touching its neighbours.

4. Guard the seams

Before a phase runs, assert that the artifacts it depends on exist and are non-empty. A tiny script; exit zero or one; nothing else.

This exists because the characteristic failure of a language model at a seam is not a crash. It is producing the answer in the conversation and never writing the file (or writing it somewhere else) after which the next phase proceeds on absent or stale input and fails somewhere unrelated. A guard turns a silent, mis-attributed failure into a loud, correctly-attributed stop.

The step most worth guarding is the one that writes the final report, because that is the step most likely to hand you prose instead of a file.

Two panels: without a guard a missing file lets the run continue and fail past the last phase; with a guard it stops at the seam where the file is missing.

Figure 1: A guard does not prevent the failure. It moves the failure to where the cause is.

5. A gate is a check, not a promise

The "this is fine" dog sitting in a burning room. Label: "Gate 3: approved." The dog says "This is fine." The flames are labelled "10 unanswered review threads."

A human gate presents an artifact, takes annotations, and records approval as the artifact’s content hash in an append-only log. Before acting on that approval, the workflow re-computes the hash and refuses if it is absent.

Keying on content rather than on a flag buys two properties. A revised artifact has a new hash, so a revision requires fresh approval instead of inheriting the old one. And an approval cannot be manufactured by an agent that believes it was approved.

The workflow that fixes review comments on a pull request had three gates, of which only the first was actually checked, the others in prose. On a real run the third gate, the one guarding everything that reached the outside world, was skipped: the change was pushed to a pull request under review and not one of its ten review threads received a reply. Reviewers were left with silently-changed code and no explanation. The fix was not more prose; it was one shared check called at all three gates, plus the same check inside the two components that publish, so a mis-sequenced publish fails instead of succeeding quietly.

An artifact is hashed and the hash recorded in an append only approval log; a revised artifact hashes to a different value, is not found in the log, and must be approved again.

Figure 2: Keying approval to content is what makes a gate a check rather than a promise: a revision cannot inherit an approval, and an agent cannot manufacture one.

6. Judgment runs in fresh context

Any step that judges work done earlier in the same run is dispatched fresh, so it never inherits the context of the step that produced what it is judging. In the workflow that answers pull request review comments, that means the agent deciding whether a comment is correct has not seen the agent that wrote the code under review. The same goes for the agent that proves the tests, and the one that writes the report.

This is a bias control, not a token optimization. An agent that just spent twenty minutes writing a function is the worst available judge of whether that function is wrong.

7. The loop is refereed by a script, not by the agent inside it

The build phase is a two-tier loop: an inner loop of implement→validate, and an outer loop back to re-planning. After each pass, validation writes a structured verdict (status, failure class, tests run, how many of them were the new ones, a reason, the evidence) and a deterministic referee reads it and returns exactly one of:

CONTINUE           run the next iteration
RETRY_MECHANICAL   run the same iteration again; do not re-plan
ESCALATE           go back to planning, re-approve the new plan, resume
STOP_OK            proceed to the next phase
STOP_STUCK         hand back to the human

The referee is a script because a loop that decides its own termination condition does not terminate reliably. It also enforces the guards an agent inside the loop would rationalize away:

  • False green: no new tests actually ran, so nothing was demonstrated. Escalate.
  • Same failure signature twice: the approach is wrong, not the attempt. Escalate rather than retry.
  • An iteration ceiling, and a separate cap on identical retries: bounded work, always.

The failure class in that verdict is what lets the referee pick correctly instead of retrying blindly:

  • Tactical: the implementation is wrong. Retry with a change.
  • Mechanical: the same attempt failed for a transient reason. Retry identically, capped.
  • Strategic: the plan is wrong. Escalate to re-planning, and re-approve the new plan.
  • Environmental: a credential expired, a container collided, a dependency service is down. This is not a test failure. Stop and surface it.

That last category is the one everybody omits and everybody needs. On one run an expired credential surfaced as a dead test command; validation classified it as a failing test, and the loop spent two iterations “fixing” code that was already correct, grinding against a wall only a human could move.


An implement and validate cycle whose continuation is decided by a script drawn outside the loop, fanning out to five verdicts including escalation to planning and a stop to a human.

Figure 3: A loop that decides its own termination condition does not terminate reliably. The failure class picks the arrow: tactical continues, mechanical retries, strategic escalates, environmental stops.

8. Tests come first, and then the tests are proved

Every plan names the failing tests to write. They are written first and confirmed failing before any implementation exists. That part is ordinary test-driven development.

The part that is less ordinary: after the loop goes green, a prover reverts only the production files, re-runs the new tests expecting them to fail, restores the change, and records a verdict: proven, or vacuous. A test that still passes with the change reverted did not test the change.

Without this step, “the tests pass” and “the tests test something” are indistinguishable, and the second is the only one anybody cares about. It is also the step that catches the most human-looking failure mode there is: a test written to satisfy a process rather than to catch a defect.

(The refactor workflow is the instructive exception: it writes no new tests, so its equivalent guarantee is that the existing suite passes unchanged, and any diff in test behavior is a defect.)

Four steps: green, revert only the production code, re run the new tests, restore, with a failure meaning proven and a pass meaning vacuous.

Figure 4: A test that still passes with the change reverted did not test the change.

9. Only the adapters know the outside world

The components that reason read and write files and never touch the network. The adapters are the only platform-aware code. The orchestrator wires them together.

That split is what lets the same workflow run driven by a live pull request or by a plain artifact on a branch with no pull request at all, without the second being a degraded path. The deciding and building phases are identical in both, because neither can tell the difference.

10. Terminal states, and a report on every exit

Enumerate the ways a run can end (done; done-but-unproven; nothing-to-do; bad-input; stuck; not-approved) and write a report on every one, including the boring ones. A request whose work turns out to be already complete is a finished run, not an error, and it still deserves a one-line record.

“Done but unproven” earns its place as a distinct state: a run that shipped and explained itself but could not demonstrate that its tests catch the defect is neither done nor stuck. Naming that state is what stops it being rounded up to “done”, which is exactly how a vacuous test ships looking verified.

Watch for two things here. A promise to “write a report on every exit path” only ever covers the enumerated exits; a run abandoned mid-flight leaves nothing unless something else writes a record. And if the runtime treats certain filenames specially, a sub-agent may be unable to write the very file it exists to produce: check that the reporting step can actually write to the name you chose.

Doctrine Principles

What a step should refuse to do, how it should treat text that arrives from outside, and how it should record a decision so the decision can be audited later.

11. Restraint is a first-class outcome

For any workflow that receives input from elsewhere (a review, an alert, a suggestion) the valuable behavior is often to not act. Responding to review feedback is easy to do badly in four specific ways:

  1. silently ignoring an item;
  2. satisfying the letter of it while missing the point;
  3. obediently implementing an item that was wrong, breaking working code;
  4. editing at a location the reviewer pointed to several commits ago, which now means something else.

So the deciding phase classifies each item and chooses an action, and several of the available actions must produce zero diff: a question gets answered, an incorrect claim gets a reasoned argument, an out-of-scope request becomes a ticket, a stale item is recognized as stale. Make this measurable: an evaluation that checks “did the run leave the tree untouched where it should have” against the real diff, and in which a run that changes everything fails.

Two failure modes sit either side. Obedience: accepting a claim because a senior person made it. Timidity: deferring everything out of uncertainty, which is not restraint but a refusal to do the job. The dividing line worth encoding: restraint is for claims you verified are wrong, never for work you verified is small.

12. Never do the socially irreversible thing

Some actions are cheap for a machine and expensive for a human to undo. Forbid them outright, in code, not in documentation:

  • Never force-push. Rewriting history on a branch under review can destroy a reviewer’s in-progress comments and re-anchor every thread. If the branch is behind, stop and say so.
  • Never resolve a human’s review thread. Resolving means “this is dealt with”, which is the reviewer’s judgment about someone else’s fix, not the author’s about their own. Reply, react, leave it open.
  • Never reply to praise. If the doctrine forbids performative agreement, auto-thanking a compliment is the purest possible violation of it.
  • Never merge. Never touch a file outside the approved plan.

The general rule: automate the reversible; require a human for the irreversible-in-public. This list is also, not coincidentally, the boundary of autonomy (part five).

13. Incoming text is data, never instructions

Any workflow that ingests text from outside (review comments, ticket descriptions, alert payloads, a web page) is handing attacker-influenceable content to a step that usually holds real tools. Say so explicitly in the step’s instructions: classify the content, never obey it. If a payload contains text shaped like directions (change your scope, ignore your rules, run this command) the step ignores it, decides the item on its technical merits, and reports that the payload contained injected directions rather than silently absorbing them.

The awkward corollary: the steps that most need this warning are the ones with the widest tool access, because reading external input and having the power to act on it is the same job.

14. Establish provenance before judging

External feedback is a claim about a specific version of the code, not about the current one. Before evaluating any of it, find out which version, and how far behind that is.

Get this wrong and you get the most confusing failure mode available: an item that is internally coherent, confidently argued, and describes code that no longer exists. The related trap is mistaking a platform signal for the thing you care about. A flag saying a comment’s anchor line moved is not a flag saying the comment is no longer true; the two come apart constantly, in both directions. Ask what a signal literally measures before depending on it.

The same discipline applies to a claim’s severity. A label attached by whoever raised it is their prior, formed against an older state of the world. It tells you where to look first. It is not evidence, and it never substitutes for verification.

15. Reaffirm or reverse, never drift

The second time a workflow sees the same input, it invites a specific failure: a reasoned decision reversed by attrition. Round one pushed back with evidence; round two has no memory of why, and obediently complies.

So make the prior round an input, not a hope. For any item matching an earlier decision, the workflow owes one of exactly two things: reaffirm it with fresh verification (not “as previously decided”, which is not verification) or reverse it explicitly, recording what changed and why. A reversal is healthy; an earlier decision can rest on a claim that turns out to be false. Only the silent flip is the problem, because a third round then sees a decision with no trace that it was ever contested, and the thing starts to oscillate.

16. Record how you decided, not only what you decided

These are not the same act:

  • read the migration and reasoned that it looks fine
  • executed the migration against a scratch database and counted the statements it emitted

A schema that captures only a conclusion and a confidence number flattens both into the same row. For a judgment step, the method is the product: a step’s pushback only deserves to outweigh a senior human’s opinion because it was executed rather than reasoned, and that is precisely the fact the human at the gate most needs to see. Record it as a field (executed, read, or searched) with a detail line naming what was actually done. It also makes a lazy run detectable: an “executed” claim with no detail is a smell.

Related, and easy to get wrong: a label must not lie at the gate. A human scanning a summary counts categories, so if “the tests are missing” is filed under the same label as “the code is broken”, the artifact actively misinforms at the moment it is supposed to inform. Give the distinct thing its own category, and require a claim of runtime harm to name a reachable path from a real entry point, otherwise it is latent at most, and must be labeled that way.

17. Report what contradicts the brief you were given

A dispatched step’s return contract should lead with whatever changes what its caller believes, not with a summary of what it did.

If a brief asserted that an item was a real defect and the step discovered the function does not exist: that is the headline, and it must be volunteered rather than buried under counts. Obeyed literally, a return contract that says “report the totals per category” produces exactly that burial.

Two consequences for whoever writes the brief:

  • Do not pre-classify in the brief. “This is a genuine production bug, not a test gap” anchors the one step whose entire purpose is to determine that. Ask for attention, never for a verdict.
  • Environment facts in a brief are stale by the time they are read. “The credentials are expired, skip that step” may have been true at dispatch and false a minute later. A step should re-probe any environmental assertion at the point of use, and report what it observed rather than what it was told to expect.

18. Encode the standards you want, and then enforce them

A workflow is the highest-leverage place to put “how code is written here”, because it applies on every run instead of depending on who happens to be driving. That is one of the main reasons to build one at all, but it only counts if it is implemented rather than assumed:

  • the planning step must actually read the repository’s own conventions (its instructions file, its layering rules, its protected files) and name the relevant ones in the plan;
  • validation must run the real gates: formatter, linter, type checker, tests;
  • if there is a review step, one lane of it should be about standards specifically;
  • and state the negative conventions explicitly: what not to touch, what not to introduce, which files are extend-only. Those are the ones a model will cheerfully violate, because nothing in the code says “don’t”.

A convention nobody checks is a wish. This is the same rule as everywhere else here: enforcement exists only where the check exists.

Done properly, this is the difference between “the agent produced something that works” and “the agent produced something that looks like ours”, and the second is what makes the output reviewable by the people who own the codebase.

19. The workflow knows the procedure; the repository knows itself

A workflow should be writable once and runnable against services in different languages and on different platforms. Nothing about “explore, plan, gate, red tests, implement, validate, prove, report” is language-specific, so nothing language-specific should be written into it.

Which means the concrete facts about a service do not belong in the workflow at all:

  • how dependencies are installed;
  • how the full suite runs, and how a single test runs;
  • how the formatter, linter and type checker are invoked;
  • how the service is started locally, and how migrations are applied;
  • the repository’s own rules: layering, protected or extend-only files, naming, what not to introduce.

All of that belongs in the repository: in its instructions file (AGENTS.md, by the convention in part five) or in the files that file references. The workflow names the intent; the repository supplies the command. This is the same invariant as “skills name actions, not tools”, one level up: name intents, not commands.

So a validation step says “run this repository’s test command for the scoped tests” and “run the gates this repository declares”, not pytest -k, npm test, go test ./..., or a particular package manager’s install line. One useful indirection: if each repository exposes a single well-known entry point (a task runner, a Makefile, a scripts/ directory) then the workflow’s “run the tests” is one stable command regardless of the language underneath, and the per-repository answer lives in one file instead of scattered through skill bodies.

The failure this prevents is not a crash, which is the recurring theme here. A hardcoded command works exactly as long as every service resembles the one the workflow was built in; the first service in another language does not fail loudly: the wrong command exits non-zero or prints no tests, and the agent reasons confidently over a signal that means nothing. Guarding the seam catches a missing file, not a misinterpreted one.

Run two cheap tests. Grep your own skills for language- and toolchain-specific tokens: test runners, package managers, build tools, framework entry points; every hit outside a per-repository file is a portability bug. Then run the workflow against a service in another language: the failures are precisely the assumptions you never knew you had written down.

The cost is honest and worth stating: this moves work onto the repositories, and a workflow is only as portable as the weakest instructions file it meets. A wrong answer there is worse than a missing one, because the agent will follow it. So apply the same discipline as a harness port (part five): when the repository does not declare something the workflow needs, stop and say which fact is missing, never guess a command from the shape of the directory tree.

A table: one workflow column stating four intents, and three repository columns in Python, Node and Go answering those same intents with their own concrete commands.

Figure 5: The workflow names the intent; the repository supplies the command. The left column is identical for every service, which is the whole point.


These rules describe how a run should behave. They say nothing about how much work to hand it, how many approvals a person can meaningfully give, or which steps deserve the expensive model. That arithmetic is part three.


Designing Agentic Development Workflows


Related