Deterministic (ish) workflows with Claude Code Dynamic Workflows
Deterministic (ish) workflows with Claude Code Dynamic Workflows
Before you read this: this finding assumes you already know what dynamic workflows are and how to drive them. If you don’t, start with these two and come back —
- Thariq’s breakdown of the Workflow tool — the canonical “here’s what it is and how it’s meant to be used.”
- Claude Code’s Workflow docs — the official reference for the primitives and the API.
This page is about the one thing neither of them covers: the cache.
The tool, in one breath
Set CLAUDE_CODE_WORKFLOWS=1 and a new tool appears in Claude Code: Workflow. It runs a JavaScript file that orchestrates a fleet of subagents — agent() spawns one, parallel() fans a batch out, pipeline() streams items through stages, workflow() calls another workflow. The loop logic lives in the JS; the agents only do the parts that need judgment. Code controls flow, agents do judgment.
The conventional way to use it is as a batch orchestrator: fan out, verify (ideally adversarially), synthesize, done. One shot, no human in the middle. That use is real and powerful, and it’s what Thariq’s post and the official docs cover well — go read them.
This finding is about the part they don’t. What happens when you want a workflow to stop, ask a human a question, wait, and continue — across hours, across turns, without losing its place? And why is that cheap enough to be a real pattern instead of a demo? The answer to both is the same: the cache.
The one line the docs give you
The docs give you exactly one sentence about it: “the longest unchanged prefix of agent() calls returns cached results instantly.” That sentence is the entire game, and it’s doing a lot of unexamined work. We spent a session figuring out what it actually means. Three properties carry the whole thing.
The cache is chain-keyed, not flat
The naive reading is that each agent() call is cached by its own (prompt, options). Change a call, that call re-runs; everything else stays cached. That’s wrong.
The cache is chained. Each call’s cache key folds in the result of the previous call. We proved it with two runs where one agent’s prompt was byte-for-byte identical and its options were identical — and its cache keys came out different, because the call before it had returned a different result. One call diverges and every downstream call cache-misses, even the ones whose own inputs never changed. The chain breaks at the first difference and stays broken. Nothing re-converges mid-stream.
The cache is byte-exact
There is no normalization. No whitespace tolerance, no “obviously equivalent” matching. A single trailing space in a prompt is a different hash and a miss. This sounds brutal, and it is, but it’s also what makes the chain predictable: same bytes in, same result out, deterministically.
Resume is per-run, not global
resumeFromRunId reuses the same run and appends to the same journal. There’s no content-addressed store across runs — resume from the wrong run ID and you re-run everything. The cache lives inside a run, not above all runs.
The move: make the prefix free
Here is the consequence that matters. Structure the workflow so the agents running before a gate never reference the answer field in their prompts. Keep the answer state out of the early prompt strings entirely.
Now when the operator answers and the workflow resumes, you flip one argument from null to a value. The early agents’ prompts are byte-identical to last time — because they never mentioned that argument. So the whole prefix is a cache hit, instant and free. Only the agents whose prompts actually change as a function of the answer run live.
We measured it. A three-stage workflow that re-ran clean cost ~80k tokens. Structured so the answer only touched the final stage, the same resume cost ~40k — the first two stages cached, free. The cost of a human answer-and-resume is bounded by exactly the agents whose prompts depend on the answer. If none do, it’s free.
That’s the reframe that matters: resume isn’t a recovery feature. It’s how a workflow holds memory across human turns. The cache is the state. The journal is the tape. A workflow that stops, asks you something, and picks up exactly where it left off — paying only for the part your answer changed — is a state machine, and the cache is what makes it one.
What it unlocks: workflows that stop and ask
A workflow can return anything. Return { status: 'needs_input', ... } and the thing that called it — the dispatcher, which for us is Claude’s main loop — reads that status, pauses, and surfaces a question to the operator instead of treating the run as done. The operator answers; the dispatcher resumes the same run with the answer folded into the args. The string 'needs_input' means nothing to the runtime; it’s pure convention. The dispatcher is what gives it meaning. Pair that convention with the free-prefix cache and you have a workflow that loops through human gates for the price of the work each answer actually touches. We built two.
A marketing pack with one approval gate.
Repurpose one post → draft four channels in parallel → stop and ask the operator to approve or request a change → revise only what was flagged → finalize the launch kit. We ran it live. The first fan-out cost ~312k tokens across five agents. Approving one revision then cost ~62k — a single agent — because intake and the three untouched channel drafts replayed from cache for nothing. Finalize cost another ~62k. Total: ~437k tokens. Without the cache, every resume would re-run the whole fleet just to get back to where the human left off — roughly 1.1M tokens for the same three turns. The savings aren’t a one-time discount; they compound with every human turn you take.
A two-lane feature SDLC.
A classifier routes an incoming feature request into one of two lanes. A UX lane builds a working mock, then stops and asks the operator to approve it — and loops back to rebuild if the feedback says no. Once approved, a bridge hands the mock to a dev lane, which clarifies, plans (looping on its own clarifying questions), builds in an isolated worktree, reviews itself, and opens a PR. The entire thing is one resumable run; every gate is a needs_input that pauses for a human and resumes cheap. We drove it end to end for real — eight dispatcher calls, roughly 1.1M tokens — and it shipped a faithful pull request.
One discipline holds the whole pattern together, learned the hard way from an agent that reported changes it never made: verify the artifact, don’t trust the report. A gate that returns a clean, plausible summary is not evidence the work happened. Audit the diff, the git log, the actual file — make the success criterion a new commit SHA, not a build that passes.
The trade-off, stated honestly
Pulling answer-state out of the early prompts isn’t free. Sometimes an early agent legitimately benefits from knowing “this is a rework, here’s what changed” — context that would make its output tighter. You can’t have both: cache discipline and rich early context pull in opposite directions. The honest rule is per-prompt. If an early agent actually uses the rework context for something load-bearing, keep it and eat the cache miss; the cache is never worth a worse work product. If it doesn’t — and often it doesn’t — pull it out and pocket the free prefix.
The fragile boundary
One real weakness. The args you pass to a workflow are inline JSON — there’s no file-reference option. So across every resume, the dispatcher has to hand-carry the exact same args object, byte for byte, from the previous turn into the next call. Anywhere it might paraphrase a string, re-order a key, or trim a space, the cache silently misses for a reason that has nothing to do with the operator changing anything. We’ve filed the feature request: an --args-file so the canonical args can be pinned to disk and the byte path made deterministic. Until then, the discipline is on the dispatcher.
What this adds up to
The Workflow tool was built for batch jobs — fan out, verify, synthesize, done. That use is real, and Thariq’s post and the docs cover it well.
But the same tool, plus one undocumented cache, is something else: a resumable human-in-the-loop state machine that can stop, ask you something, wait however long, and continue from exactly where it paused — paying only for what your answer changed. The cache is the difference between the conventional use and ours. It’s chain-keyed, byte-exact, per-run, and described in a single sentence that nobody should have had to reverse-engineer.
Findings from six experiments in the lab, May–June 2026. Claude Code 2.1.150–2.1.158 — one harness, one window of versions; the cache behavior is undocumented enough that a later release could move underneath all of this. Every number here came out of a real run.