# Graph engineering is easy. Not blowing your budget on one workflow is the hard part

Author: Max Tytarenko | Date: 2026-07-23 | Category: AI & ML | Tags: ai-agents, claude-code, multi-agent, workflows, llm-costs
Canonical: https://www.tytarenkoagency.com/blog/graph-engineering-is-easy-not-blowing-your-budget-on-one-workflow-is-the-hard-part

> Everyone is teaching how to fan a Claude Code workflow out to 1,000 agents. Nobody teaches the chapter where the bill arrives. How a minor audit became 287 agents and 15.3M tokens, and the pre-execution guard + model routing ladder that keep my fleet honest.

X is having its "graph engineering" moment. A [viral five-step course](https://x.com/0xCodila/status/2079597821511020996) teaches you to stop chaining agent steps in a line and start drawing graphs: fan a Claude Code dynamic workflow out to as many as 1,000 agents, 16 running at once, all from one prompt. Peter Steinberger compressed the shift into one line: are we still talking loops, or did we move to graphs? And [Simon Willison's write-up of Bun's Zig-to-Rust port](https://simonwillison.net/2026/Jul/8/rewriting-bun-in-rust/) shows the ceiling of the technique: roughly 535,000 lines of Zig became over a million lines of Rust in 11 days, across about 50 workflows peaking at 64 parallel agents.

That port also cost about $165,000 in usage.

Every course covers the fan-out. None of them covers the chapter where the bill arrives. I learned that chapter the hard way, so here it is.

## The audit that became a fleet

I run a personal fleet of coding agents: an orchestrator that fans work out to subagents through scripted workflows. A workflow script is ordinary JavaScript. It declares phases, builds lists, and calls helpers like `parallel()` and `pipeline()` that spawn one subagent per list item. Write the script once, save it, and you can re-run the whole shape by name on demand. That convenience is exactly where the trap lives.

One day a saved workflow got re-run on a *minor* audit. Nothing exotic: the kind of task you'd expect to take one agent and a few minutes.

![A visual representation of a digital workflow with coding agents and fan-out connections.](https://adltqzpaelnsrebtkzfj.supabase.co/storage/v1/object/public/blog-images/ai-generated/uploads/20260723_183503_867c7105.png)

It silently fanned out to roughly **287 agents** and burned about **15.3 million tokens**.

The anatomy of the blowup is worth spelling out, because it is the same shape every time. The script iterated over a list whose size depended on the input, and for each item it ran a nested multi-stage check. A list of N items times a nested per-item stage is not N agents, it is N times the nested width. On the original scope that product was small. On the re-run scope the list was longer, the multiplication did its job, and nobody was standing between "script says spawn" and "spawn."

Here's the uncomfortable part: nothing malfunctioned. The workflow did exactly what its script said. The orchestration was correct. The *scale* was the bug.

The standard advice, and the only cost advice the graph-engineering courses give, is "start scoped and watch usage." I had read that advice. I agreed with it. It did not save me, because advice is not enforcement. A prompt-level "be careful" is a suggestion made to a system that is optimizing for task completion, and the whole point of workflows is that they run wide *without you watching*. The one thing you can't do with a fleet is babysit it; that's why you built it.

## Put the "no" in a hook, not in the prompt

My fix was structural: a pre-execution hook that intercepts every workflow launch *before it runs* and statically parses the orchestration script. Claude Code's hook system lets you register a shell command on any tool call; if the command exits non-zero, the tool call is blocked and the agent sees the message. The wiring is a few lines of config:

```json
"PreToolUse": [{
  "matcher": "Workflow",
  "hooks": [{ "type": "command", "command": "~/.claude/hooks/workflow-guard.sh" }]
}]
```

The guard resolves the workflow script from the tool call (an inline script, a script file path, or a saved workflow looked up by name) and estimates how many agents it would spawn. The estimator is about a page of Python. In pseudocode:

```text
strip comments and string contents        # so "parallel" in prose never trips the scan
estimate  = count of declared phases
          + every Array.from({length: N})
          + every parallel()/pipeline() over a list whose size is knowable:
              a literal array, a const bound to a literal, the passed-in args
            (nested fan-out multiplies: outer items x inner width)

block if  estimate > THRESHOLD                       # default 20, env-tunable
      or  a while / C-style for loop spawns agents   # no static bound
      or  an unknown-size list is multiplied
          by a nested fan-out                        # worst case unbounded

allow, but print the estimate, if a single-level fan-out
runs over an unknown-size list                       # most common legitimate shape
```

The three block rules map to the three ways a run actually gets away from you. A declared fleet over the threshold is the honest case: the script openly says it wants 120 agents, and 120 is a decision a human should make. An unbounded loop has no static ceiling at all. And an unknown-size list multiplied by a nested stage is the exact shape that got me: fine on one input, a fleet on another.

A blocked run looks like this. The message goes back to the agent, which has to stop and tell the human:

```text
BLOCKED by workflow-scale-guard: this Workflow run looks large.
  estimate: ~120 agents (threshold 20)
  signals:  3 declared phase(s); parallel() over ~40 item(s) x ~3 nested = 120
To run it anyway, re-invoke with WORKFLOW_SCALE_ACK=1 set in the environment.
```

The threshold of 20 is not a magic number; it is a policy decision I made once, deliberately, after the incident. Anything wider than 20 agents on my setup is an intentional event, so anything wider must be acknowledged by a human. Your number will differ. The point is that some number exists and a machine enforces it.

Three design decisions matter more than the parsing:

**The estimate hunts shapes, not exact numbers.** An exact static agent count is impossible; the script is Turing-complete. So the guard is a heuristic biased toward catching *runaway shapes*: the declared fleet, the unbounded loop, the unknown-list-times-nested-stage multiplication. Anything it cannot confidently flag is allowed. That bias is deliberate. A guard that false-positives on every legitimate run gets disabled within a week, and a disabled guard protects nothing.

**The escape hatch is explicit, human, and outside the agent's reach.** An intentional big run is one environment variable away (`WORKFLOW_SCALE_ACK=1`), but that variable has to be set on the CLI process by the person launching it. The agent can't talk its way past the hook, and it can't set the variable for its own already-running process. Big runs still happen; they just happen *on purpose*.

**It degrades loudly, never silently.** The real parser needs Python. On a machine without it, the guard falls back to catching only blatant declared fleets, and says so on every single run, so you know you are living with a weaker guard instead of assuming you are covered. The same honesty applies to what it cannot see: a saved workflow whose script the hook cannot read gets waved through with an explicit warning that the scale was *not* estimated. A guard that admits its blind spots gets trusted; a guard that pretends to be airtight teaches you complacency exactly where it is weakest.

## The second lever nobody mentions: stop running recon on your best model

Blocking runaway scale is half the cost story. The other half is *what model each agent runs on*. The graph-engineering courses fan everything out on whatever model your session uses, which for most people is the most capable and most expensive one. That is backwards, because in a wide run the overwhelming majority of agent-hours are spent on the dumbest work: reading files, grepping, extracting facts.

My fleet routes by stage, on a simple ladder:

- **Haiku reads.** Reconnaissance, file discovery, fact extraction: the mass stages where a fan-out spends most of its agents. In my setup this is a named read-only agent definition, so a workflow just says "use the scout" and the routing is decided once, not re-argued per run.
- **Sonnet does bulk.** Uniform worker stages: mass transforms, migrations over many files, summarization fan-outs. Routed per call, because bulk stages vary in what tools they need.
- **Opus verifies.** Adversarial verification of findings is the one place I deliberately pay for a top-tier model. A verifier that agrees with everything is decoration, so the verifier is its own agent definition: high effort, read-only, never edits, prompted to refute rather than confirm.
- **The session model thinks.** Synthesis, judgment, and final answers stay on the model I am actually talking to.

Two rules keep the ladder honest. Fan-out agents return *structured results* against a schema, not prose; every paraphrased hand-off between stages is a game of telephone that burns tokens and loses facts. And large artifacts go to a file with the path passed along, never inlined into a context window.

![An infographic showing the model-routing ladder with labeled stages and corresponding icons.](https://adltqzpaelnsrebtkzfj.supabase.co/storage/v1/object/public/blog-images/ai-generated/uploads/20260723_183319_70c0d13c.png)

I won't quote per-token prices here (they change, look up the current sheet), but the ordering is stable: the small read-tier models cost a small fraction of the top tier per token, and recon is where the token volume lives. Route a wide sweep down this ladder and the bulk of your agent-hours land on the cheap rungs. Run the same sweep entirely on your best model and an afternoon audit starts to resemble a car payment.

The two levers compose. The scale guard caps how wide a run can silently go; the ladder caps what each unit of width costs. Either one alone would have made my incident smaller. Both together are what let me keep saying yes to big runs at all, because a bounded, cheap-by-default fleet is a fleet you can afford to use aggressively.

## The missing chapter, in five lines

1. Fan-out advice without a hard budget guard is a course with the last chapter missing.
2. Put the "no" in a hook the agent can't argue with. Prompts are suggestions; exit codes are not.
3. Bias every guard against false positives. The guard that annoys you is the guard you will disable.
4. Make the escape hatch explicit and human. Big runs should be a decision, not an accident.
5. Route models by stage. Your top model should think and verify, not read files.

The graph engineers are right about the shape of the work: real tasks are graphs, and fleets are how you run them wide. Just remember that a fleet has a fuel bill, and the only version of "be careful" that survives contact with an autonomous system is the one compiled into a hook.

## FAQ

### Why a static pre-execution check instead of a runtime budget cap?

A runtime cap kills a run after the money is partly spent and the state is half-mutated; a static check costs nothing and fires before agent one spawns. The two are complementary, but the static check is the one that turns an accident into a non-event.

### Doesn't a 20-agent threshold get in the way of legitimate big runs?

Rarely, and that is by design. The most common legitimate pattern (a single-level fan-out over a list of unknown size) is allowed and merely reported. When I genuinely want a hundred-agent sweep, setting one environment variable is a five-second cost for the guarantee that no such sweep ever happens by accident.

### Where do the biggest savings from model routing come from?

From volume, not from any single call. Recon and extraction stages account for most agent invocations in a wide workflow, and they need the least intelligence, so moving them to the cheapest tier moves most of your token volume to the lowest price point while quality-critical verification stays on the strong model.

## Related reading

- [Context Engineering for Vibe Coding: Structured Prompts for Rapid SaaS Prototyping](https://www.tytarenkoagency.com/blog/context-engineering-for-vibe-coding-structured-prompts-for-rapid-saas-prototyping)
- [AI Automation Architectures for Bootstrapped SaaS: Open-Source Patterns to Match Enterprise Scale](https://www.tytarenkoagency.com/blog/ai-automation-architectures-for-bootstrapped-saas-open-source-patterns-to-match-enterprise-scale)
- [Micro-SaaS Automation Architectures: AI Patterns for Solopreneur Scalability](https://www.tytarenkoagency.com/blog/micro-saas-automation-architectures-ai-patterns-for-solopreneur-scalability)

