← blog.wrbriggs.cloud
DYNAMIC WORKFLOWS IN CLAUDE CODE · PART 3 OF 6

Pipelines, Barriers, and Verification: Orchestration Patterns That Actually Converge

How to structure multi-stage fan-outs, encode verification that actually filters, write loops that actually terminate, and know what a stopped run keeps.

Will Briggs·August 2026·17 min read

BEFORE YOU START

This post assumes part one (the mental model) and part two (script anatomy, schemas, the authoring loop). You should be comfortable opening a persisted script and editing it.

Two versions of the same 40-file audit. Same agents, same prompts, same schemas. The first was written the way most people write their second workflow: fan out, gather everything, transform, fan out again. The second was pipelined. In a community benchmark of exactly this comparison, the barrier-everywhere version cost roughly twice the tokens and three times the wall-clock. Nothing about the work changed. Only the shape of the run did.

That's the thesis of this post: structure is a cost lever and a quality lever, and it's the same lever. The first half is about shape: where the barriers go, how loops end. The second half is about the quality machinery good shape makes affordable, meaning verification that filters instead of decorating, and termination that holds at round 20, not just round 2. The deeper economics (why fan-out changes the cost curve itself rather than one bill) opens part six.

The two primitives, precisely

Everything multi-stage in a workflow is built from two calls, and the names mislead people in a specific way, so let's get the semantics exact.

pipeline(items, ...stages) runs every item through all stages independently, with no barrier between stages. Item A can be in stage 3 while item B is still in stage 1. Wall-clock cost is the slowest single-item chain, never the sum of the slowest item per stage. Each stage callback receives (prevResult, originalItem, index), and the second two arguments matter more than they look: they let a later stage label its work by filename or position without threading that context through stage 1's return value. A stage that throws drops its item to null and skips that item's remaining stages.

parallel(thunks) runs zero-argument async functions concurrently and is a true barrier: it waits for every thunk before returning anything. A thunk that throws resolves to null in the results array; the call itself never rejects, which is why .filter(Boolean) follows it everywhere. Results come back in input order regardless of finish order.

PIPELINE: NO BARRIER BETWEEN STAGES
item 1 item 2 item 3 item 4 item 5 done: slowest chain
BARRIERS: EVERY STAGE WAITS FOR EVERYONE
item 1 item 2 item 3 item 4 item 5 done: later pale = idle, waiting on the slowest sibling
The same five items, same stage durations. Amber, green, and slate are stages one through three; dashed lines are barriers. The gap widens with item count and duration variance.

The composition move that makes both click: a pipeline stage can contain a parallel. Per-item fan-out inside a no-barrier flow. That's exactly what verification looks like later in this post: each file's findings get checked concurrently the moment that file's audit lands, while other files are still being audited.

OBJECTION

“parallel() is the parallel one; pipeline() is the sequential one.” Both run agents concurrently, up to the same cap. The names describe synchronization, not concurrency: parallel is a barrier that gathers everything before anyone proceeds, pipeline is a flow that never makes an item wait for its siblings. Read them as “the fast one” and “the slow one” and you'll pick wrong every time.

When a barrier is earned

A barrier is correct in exactly three situations, and all three share one property: stage N needs cross-item context from all of stage N−1.

Deduplication and merging is the first: you can't dedupe findings until every finder has reported. Early exit on totals is the second: “zero findings, skip verification entirely” requires knowing the total is zero. The third is a stage whose prompt genuinely references the other items. “Rank these twelve findings against each other” can't run per-item by definition.

Everything else is an unearned barrier, and there's a smell-test that catches most of them. If your script reads

const a = await parallel(...)        // barrier
const b = a.flat().filter(Boolean)   // plain transform, no cross-item logic
const c = await parallel(b.map(...)) // barrier again

that middle line doesn't need the first barrier. Anything that operates item-by-item (a flatten, a map, a filter) belongs inside a pipeline stage:

const c = await pipeline(items,
  item => agent(...),                          // stage 1
  (result, item) => result && agent(...))      // stage 2 runs per item, immediately

The fast items start stage 2 the moment their own stage 1 finishes, instead of waiting for the slowest sibling. That's the whole refactor, and it's the one the cold-open numbers are pricing: the 2× token, 3× wall-clock gap was this pattern, unapplied, across three stages.

Two justifications that don't survive contact with the timeline: “the stages are conceptually separate” (structure follows data dependencies, not concepts) and “the barrier version is cleaner.” It is cleaner. You're paying idle time at full price for the aesthetics.

Why verification works: the attention argument

Part one made a claim and deferred the proof: quality comes from verification structure, not agent count. Here's the proof, because the patterns in the next section should read as engineering, not ritual.

Three mechanisms, all properties of how models handle long contexts. First, recall is positional: models retrieve material from the start and end of a long context far more reliably than from the middle, the lost-in-the-middle result. A monolithic audit puts file 12's suspicious pattern precisely where recall is worst. Second, attention is a budget. Everything in a context competes for it, so a context holding one file and one question spends nearly the whole budget on the thing that matters, while a context holding forty files spends it everywhere. Third, anchoring: what a model concluded about file 3 shapes how it reads file 30, and inside a single context there's no way to un-see a prior conclusion.

Now run those three mechanisms backward and verification design falls out on its own. Independence requires context isolation. A verifier that shares the auditor's context inherits the auditor's anchoring; it has already seen the reasoning, so it verifies nothing. A verifier that receives only the claim (file, line, severity, and not one sentence of the reasoning that produced it) starts with clean attention, no sunk conclusions, and an explicit instruction to refute. This is also why part two spent so long on schemas: a structured claim is a transportable claim. You can hand {file, line, issue, severity} to a skeptic without smuggling the auditor's thinking along with it.

There's a name for the design principle hiding in all this, and it'll be familiar: single responsibility. One agent, one question, one small context is SRP applied to attention instead of code. I'll stop at the S. Mapping the rest of SOLID onto agents is the kind of analogy that ends careers.

OBJECTION

“Verification is just redundancy.” Three identical refuters share failure modes: same lens, same blind spots, correlated errors. Redundancy narrows variance on one question; diversity widens coverage across questions. A correctness lens, a security lens, and a does-it-reproduce lens catch what three copies of one prompt can't. Pick per finding type: redundancy when the question is binary and noisy, diversity when a finding can be wrong in more than one way.

Quality patterns as code

The spine script from part two ends with 60-odd findings across 38 files, every one shaped correctly, some of them wrong anyway. Here's the Verify phase that decides which:

const LENSES = ['correctness', 'security', 'reproduce']

const verified = await pipeline(found.files,
  file => agent(`Audit ${file} for missing auth checks.`, {
    label: file, phase: 'Scan', schema: AUDIT_SCHEMA,
  }),
  audit => audit && parallel(audit.findings.map(f => () =>
    parallel(LENSES.map(lens => () =>
      agent(`Through the ${lens} lens, try to REFUTE this claim: ` +
            `${f.issue} at ${audit.file}:${f.line}. Default to refuted ` +
            `when uncertain.`, {
        label: `${audit.file}:${f.line}`, phase: 'Verify', schema: VERDICT_SCHEMA,
      })))
      .then(votes => ({ ...f, file: audit.file,
        killed: votes.filter(Boolean).filter(v => v.refuted).length >= 2 }))
  )))

const confirmed = verified.filter(Boolean).flat().filter(f => !f.killed)

Read the last line first. Findings are claims; votes are data; the gate is an if in all but syntax. No model decides whether verification happens: the script counted refutations, and two out of three kills. That's what “the plan lives in code” buys at the quality end, a gate no agent can talk its way past.

Three details in the excerpt repay attention. Each refuter gets the claim and nothing else: the attention argument, operationalized. Each gets a distinct lens, per the misconception box above. And each carries phase: 'Verify' explicitly, because inside concurrent stages the global phase() call races; two stages running at once would fight over it. Same string, same group box in /workflows. Set it per call and the race disappears.

The rest of the pattern family, briefly, because they're all the same move (generate, independently check, count in code) pointed at different problems. A judge panel handles open-ended work where there's no binary claim to refute: three independent attempts at a plan from named angles, parallel judges scoring each, synthesis from the winner. It beats one-attempt-iterated whenever the solution space is wide. A completeness critic is a final agent asking one question (what's missing?) whose findings become the next round's work-list rather than a report footnote. And no silent caps is part two's log() honesty rule promoted to a pattern: any top-N, any sampling, any truncation gets logged, because a run that silently audited 50 of 200 files reads as full coverage to whoever receives the report.

Cost honesty before moving on: verification multiplies agent count. The 38-agent scan became roughly 90 agents with the verify phase attached. That's the number the launch-thread critics called tokenmaxxing, and from the inside it's the most defensible spend in the run: every agent added after the scan exists to shrink the pile a human has to review. Part six does that arithmetic properly.

Loops that terminate

Unknown-size discovery (find the flaky tests, find the injection risks, keep going until there's nothing left) is the task shape turn-by-turn prompting handles worst, because “keep going until” is precisely what a model won't reliably do. In a workflow the loop is code, which means termination is a design decision you make once, explicitly.

The workhorse is loop-until-dry: rounds of finders, stopping after K consecutive rounds surface nothing new.

const seen = new Set(), confirmed = []
let dry = 0
while (dry < 2) {
  const found = (await parallel(FINDERS.map(f => () =>
    agent(f.prompt, { phase: 'Find', schema: BUGS_SCHEMA }))))
    .filter(Boolean).flatMap(r => r.bugs)
  const fresh = found.filter(b => !seen.has(key(b)))
  if (fresh.length === 0) { dry++; continue }
  dry = 0
  fresh.forEach(b => seen.add(key(b)))
  confirmed.push(...await verify(fresh))   // the verify phase above, as a function
  log(`round done: ${fresh.length} new, ${confirmed.length} confirmed`)
}

Now the bug, because I wrote it before I understood it. My first version had no seen; I deduplicated incoming findings against confirmed, which felt equivalent and saved a variable. Round 1: forty findings, twelve confirmed. Round 2: the finders resurface some of the twenty-eight the judges killed, they're not in confirmed, so they count as fresh, get re-verified, get re-killed. Every round after that, the same zombies shamble through the verify phase at three agents apiece, dry never increments, and the loop runs until the 1,000-agent backstop taps it on the shoulder. In a two-round test the bug is invisible. At round 20 it's most of your bill. The fix is the one line I'd deleted: track everything surfaced separately from everything that survived, and dedupe against the first.

Budget guards are the other exit condition, and they compose with any loop. The budget global exposes {total, spent(), remaining()}: the token target from the user's directive, shared across the whole run, and hard. At the ceiling, further agent() calls throw rather than politely overspending. The idiom has a null-guard for a reason:

while (budget.total && budget.remaining() > 50_000) { ... }

Drop the budget.total check and an unbudgeted run makes remaining() infinite; the loop sprints straight at the agent cap. Which, to be clear about its role: 1,000 agents per run is a runaway-loop backstop, not a design parameter. A loop that touches it has already failed. The exit condition's job is to fire long before.

OBJECTION

“The model will stop when it's done.” “Done” is not a model state you can build on. Part one's example was the loop that quit after two iterations because the model felt finished. In a workflow, termination belongs to the script: a count, a dry-round counter, a budget floor. If you can't name the exit condition, the loop isn't designed yet; it's just running.

Resource mechanics: the options, motivated

Part two's option table deferred four rows here. Each one answers a specific problem, so here they are problem-first.

You passed 200 items. Is that 200 concurrent agents? No. Concurrency caps at min(16, CPUs − 2) per workflow; everything past the cap queues invisibly and runs as slots free. (A single call accepts up to 4,096 items, and passing more is an explicit error rather than silent truncation.) Design consequence: item count is a scope decision. Concurrency isn't yours to manage.

Why do identical agents cost less than varied ones? Agents that share model, effort, agent type, tools, schema, and working directory build the same prompt prefix, and the runtime deliberately holds a fan-out's siblings until the first agent's response begins so the rest read its cache. Uniform configs inside a fan-out are cheaper: vary the prompt per item, keep everything else identical, and save the variation for stage boundaries.

Some stages are trivial and some are hard. Why pay one rate? Route per stage. effort: 'low' for the mechanical work (listing files, extracting strings), high effort or a stronger model for the judges whose votes kill findings. The one rule is to route by stage, not per-agent whim, or you fragment the cache you just read about.

Two agents need to edit files at once. Won't they conflict? That's the one job for isolation: 'worktree': each agent gets its own git worktree, edits in parallel without collisions, and the worktree evaporates if unchanged. It costs real per-agent setup, so it's for mutation only. A migration transforming 80 files in parallel, yes; anything read-only, never. And when a stage wants a curated persona rather than the default worker (a code-reviewer with its own system prompt), agentType mounts it, and composes with schema.

What a stopped run keeps

The runtime journals every agent() call and its result as the run progresses. Resume re-executes the script from the top and serves completed calls from the journal, and the cache is keyed per call on (prompt, options). That keying explains two things you already know: why part two banned Date.now() (replay must regenerate the same call sequence), and why editing a script invalidates only from the first changed call.

It also has a sharp edge, the replay-order rule. Cached results stop at the first agent that didn't finish, and everything started after it reruns, even if it completed. Four agents, A through D, started in that order; stop the run while B is mid-flight:

A finished · cached
B mid-flight · reruns
C finished · reruns anyway
D finished · reruns anyway
Replay in start order: the cache stops at the first unfinished agent. C and D completed, and rerun anyway because they started after B.

Stopping a 16-wide fan-out at agent 3 forfeits most of the fan-out. The lesson is about where to stop, not whether: pause at quiesced boundaries (between phases, after a stage drains), never mid-flight on an impulse.

One honest limit: the journal lives with the session. Exit Claude Code and resume is gone; a minutes-scale pause is safe, an overnight one is a fresh run.

Those mechanics unlock the pattern from part one's fit test, so here it is with its machinery showing. A running workflow takes no input. But a script can return early, carrying its partial results and the decision it needs, and relaunch with your answer in args. Upstream calls have unchanged prompts and options, so they replay from cache; re-entry costs approximately nothing. Two rules keep it true: upstream prompts must not depend on the answer (or the cache invalidates from there), and the checkpoint must land where every started agent has finished (or the replay-order rule collects its toll). Granularity is the design question: a sign-off between stages is cheap and legitimate; an escalation per item is turn-by-turn conversation rebuilt with extra steps. The full treatment (long-lived approval gates, replay edge cases) is its own post, queued after this series.

What to do next

The checklist, compressed to a paragraph: pipeline unless a stage needs everything, and make it prove it. Barriers only for dedup, early exit, or genuinely cross-item prompts. Verifiers isolated, lenses diverse, votes counted in code. Loops with a named exit condition, deduped against seen, floored by budget. Configs uniform inside a fan-out; effort routed by stage; worktrees for mutation only. Stops at quiesced boundaries; checkpoints where the cache makes re-entry free.

Homework builds directly on part two's. Take your saved audit workflow and attach a Verify phase: three refuters, distinct lenses, majority kill, phase: 'Verify' on every call. Run it on the same directory as last time and diff the findings lists. The shrinkage is this post's entire argument, measured on your own code.

Part four changes the subject from writing workflows to owning them: naming, argument contracts, review, version pinning, and shipping the result in a plugin so the workflow you just built stops being yours and starts being your team's.

← Part 2: Anatomy of a Workflow Script
Part 4: From One-Off Script to Team Asset: Saving, Versioning, and Shipping Workflows in Plugins →

Written against Claude Code v2.1.239 and the Agent SDK's Workflow tool reference. The changelog tracks what's moved since.