Home / Blog / Harness patterns

Harness patterns

Agent Pipeline vs Barrier: Where to Put the Gate

A pipeline moves work forward; a barrier stops the work that should not move. The skill is placing each one where it earns its keep, and not on every step.

Most agent systems fail in one of two shapes. Either the agent runs free and does something you did not want, or it stops dead at a gate that should have let it through. The first is a pipeline that never checked itself. The second is a barrier that checks everything and trusts nothing. Both come from the same mistake: treating control as a single setting instead of a property you place where it belongs.

Here is the framing we use in production: a pipeline moves work forward, and a barrier stops work that should not move. You need both. The skill is putting each one where it earns its keep, and not where it just slows you down or hides a failure.

The pipeline is the default, and that is the problem

When you wire up your first agent, everything is a pipeline. Input goes in, the model decides, a tool runs, output comes out. It feels great in a demo. The failure mode shows up the first time the agent does something with a real consequence: it deletes the wrong rows, posts a draft to a live audience, or spends money on an API call you did not budget for.

The instinct then is to wrap the whole thing in approval. Every step waits for a human. That is the barrier overcorrection, and it kills the reason you built an agent in the first place. Now you are the bottleneck on every loop, and the agent is a very expensive form-filler.

The useful question is not "pipeline or barrier." It is "which actions are reversible and internal, and which ones reach outward or cannot be undone." That line is where the barrier goes. Everything inside it is pipeline.

The test that decides where the barrier goes

We run a two-part test before any action the agent takes:

  1. Does the effect stay inside our own systems? Editing code, writing to a scratch database, running a build, reading a file. If yes, it is a candidate for pipeline.
  2. Can we cleanly undo it before anyone outside is affected? A git revert, a dropped test row, a container restart. If yes, it stays pipeline.

Both yes means the agent runs it and reports after. Either no means a barrier: a dry run, a check against a test recipient, or proof that the undo exists before the action fires. The actions that reach a customer, move money, or delete real data are the short list that gets gated. Everything else flows.

The payoff is concrete. If most of an agent's actions are reversible and internal, and only the small remainder are gated, the agent does most of its work unattended and you review the part that actually carries risk. The exact ratio depends on your system; the shape is what matters. Gate by consequence, not by step count.

What a barrier should actually do

A barrier is not "ask the human." That is the laziest version, and it does not scale. A good barrier does one of three things, in order of preference:

Prove the undo. Before a destructive database change, run the same WHERE clause as a SELECT and confirm the row count. If the agent is about to update 3 rows, the count comes back 3, not 30,000. The barrier is a check the agent runs itself, not a wait for you.

-- barrier: confirm scope before the write
SELECT count(*) FROM orders WHERE status = 'pending' AND created_at < now() - interval '90 days';
-- agent asserts the returned count matches its plan, THEN runs the UPDATE

Route through a test path. For anything customer-reaching, the barrier sends to a fixture recipient or through the product's own approval flow first. The agent still acts; the action just lands somewhere safe until it is cleared. This is how an autonomous loop can touch a live channel without you holding its hand on every send.

Escalate with a recommendation. Only when there is no undo and no test path does the barrier stop and ask. And when it asks, it brings the decision pre-chewed: here is what I want to do, here is why, here is what I will do by default if you do not veto. A barrier that just says "proceed?" wastes the one resource you cannot scale, which is your attention.

Failure modes we have hit

The barrier that became a pipeline. We had an approval gate that auto-approved after a timeout, meant as a convenience. Under load it approved everything, because nobody answered in time. A gate with a default-yes timeout is not a gate. If a barrier can be satisfied by waiting, it is decoration.

The pipeline that needed a barrier. An agent with file-write access and a loop will, given enough iterations, write somewhere you did not expect. We caught one writing into a sibling worktree and corrupting an unrelated checkout. The fix was a barrier on write scope: the agent gets an explicit path it is allowed to write under, and a write outside it fails loudly instead of succeeding quietly.

The barrier nobody could see. Early on, our gate logic lived inside one agent's prompt. When a second agent came online it had no idea the gate existed and routed around it. Barriers belong in shared, enforced infrastructure (a hook, a wrapper, a policy check in the tool layer), not in a single agent's instructions. If the barrier is advice, it is not a barrier.

How to wire it

The cleanest place for a barrier is the layer every action already passes through. If your agents call tools through a single dispatch function, that function is where you classify the action and decide pipeline or barrier. You do not want this logic sprinkled across prompts, because prompts drift and prompts are per-agent.

// pseudocode: one classifier, applied to every tool call
function gate(action) {
  if (action.reversible && action.internal) {
    return run(action);                  // pipeline
  }
  if (action.hasTestPath) {
    return run(action, { target: 'fixture' });   // barrier: test path
  }
  if (action.canProveUndo) {
    return run(action, { requireUndoProof: true }); // barrier: prove undo
  }
  return escalate(action);               // barrier: ask, with a recommendation
}

Two properties make this hold up. The classifier is the same for every agent, so a new agent inherits the rules instead of relitigating them. And the default for an unclassified action is the barrier, not the pipeline. When you are not sure, you stop. An agent system that defaults to "run it" will eventually run the thing you did not want.

The short version

A pipeline moves work; a barrier stops the work that should not move. Place the barrier on the line between reversible-internal and outward-or-irreversible, not on every step. Make the barrier do real work (prove the undo, route to a test path, or escalate with a recommendation) instead of just asking. Put it in shared infrastructure so every agent obeys it, and default unknown actions to the barrier. Get that boundary right and the agent does most of its work on its own, while the actions that can actually hurt you are the ones that wait.

Get the drops.

New patterns, Claude coverage, and field notes as they land, plus first access to the kits.

Email to subscribe