# Graph engineering — deciding the shape of the work before doing it

**Load this when** a job has more than two steps and you are about to write them in a
line: choosing between a chain and a graph, finding the dependencies that are not real,
placing a check between a parallel layer and the node that consumes it, or deciding
whether the structure may be discovered while it runs.

**Spec pinned:** *Graph Engineering with Claude*, `https://x.com/Mahaximus_/status/2082442856417956173`
(published 2026-07-29); Claude Code `CHANGELOG.md` v2.1.154 – v2.1.229, and the Workflow
tool contract as presented in-session at CLI **2.1.223** · read 2026-08-15

`SKILL.md` §2 owns the tool-calling loop and §5 the multi-stage pipeline. Both assume
the shape is already decided. **This file is how it gets decided**, and it is upstream of
every constant in the rest of the pack: a threshold tuned inside the wrong shape is a
well-measured answer to the wrong question.

## Contents

- [The source, and what this file adds](#the-source-and-what-this-file-adds)
- [1. Node and edge](#1-node-and-edge)
- [2. Your loop is already a graph, and most of its edges are fake](#2-your-loop-is-already-a-graph-and-most-of-its-edges-are-fake)
- [3. The fake-edge test](#3-the-fake-edge-test)
- [4. The diamond](#4-the-diamond)
- [5. Two ways a diamond fails silently](#5-two-ways-a-diamond-fails-silently)
- [6. The checker node](#6-the-checker-node)
- [7. Static or dynamic](#7-static-or-dynamic)
- [8. When not to build a graph at all](#8-when-not-to-build-a-graph-at-all)
- [9. What Claude Code actually executes](#9-what-claude-code-actually-executes)
- [10. Barrier or no barrier](#10-barrier-or-no-barrier)
- [11. Project defaults, written once](#11-project-defaults-written-once)
- [11a. MAST — the failures, named from outside](#11a-mast--the-failures-named-from-outside)
- [12. The source's four diagrams, and what each one is for](#12-the-sources-four-diagrams-and-what-each-one-is-for)
- [Where this file disagrees with its source](#where-this-file-disagrees-with-its-source)

## The source, and what this file adds

The model below — node, edge, the fake-edge test, the diamond, the checker node, static
versus dynamic — is taken from the article pinned above. It is the clearest short
statement of the idea available, and the link is kept so the original can be re-read
rather than remembered through this summary.

Four things are **this pack's**, not the source's, and each is marked where it appears:

| Added here | Why the source could not carry it |
|---|---|
| §9 — what the host actually runs, with version evidence | the article's one operational claim aged out six weeks after publication (see §9) |
| §10 — the barrier distinction | the article's diamond has a barrier at every convergence; most convergences do not need one |
| §6 — what a checker costs, and when it is a rubber stamp | a check nobody measures is a node that always says yes |
| §7 — the auditability rule as a **hard** rule, not a preference | this pack's own doctrine is that a green nobody watched fail is not evidence |

## 1. Node and edge

**A node is one unit of work, and it has five fields.** One input, one job, one output,
**one owner**, and **its own completion test**. Not *"research the topic, summarise it,
and check the sources"* — that is three nodes wearing one name. The smaller and more
defined the job, the more useful the node, because a node is also the unit you retry,
cache, review and replace.

<!-- node-contract: input, job, output, owner, check -->

The first three get drawn every time. The last two are the ones a graph is usually drawn
without — this file shipped without them until 2026-08-19 — and each has a failure that
surfaces only at the convergence, where it is cheapest to mistake for success.

**One owner — exactly one node is answerable for a given artifact.** Not one *agent*: the
same subagent may own several nodes, and a node owned by a person is still a node. The
unit of ownership is the artifact, and the assignment is made when the layer is built, not
discovered when the writes come back.

**Ownership is not what `isolation: "worktree"` gives you, and the two are complementary
rather than alternatives.** §9's primitive table calls a private checkout the only safe way
to fan out writers, and against a *race* that is true. Isolation answers **when**: two
writers cannot corrupt one file, because neither can see the other's copy while it runs.
Ownership answers **whose**: which node's version is authoritative once the branches
return. Take isolation without ownership and nothing is lost during the run — the loss
moves to the merge, which is the quieter place for it, because a convergence that silently
takes one side is indistinguishable from a convergence that only ever had one answer. §4's
first rule is the *detection* half of this: two independent workers writing one file are
one node with a race in it. Ownership is the *assignment* half, and it is what makes
fanning writers out decidable rather than merely survivable.

**Its own completion test — the node names what will close it, before it runs.** One
string: a command a verifier can run, or a named judgement standing in where no command
can, with the judge named and the verdict recorded *as* judgement. Whatever it prints is
that node's evidence row.

The field's name across this family is **`check`** — `task-pipeline`'s node schema
(`graph.schema.json`) carries it as one required string per node and exempts only a
`parked` one, the single node nobody intends to close. Same name and same meaning here,
one string and never a list, because a node needing two unrelated checks is a node doing
two jobs and the answer is to split it. One gate built from two commands is `a && b`,
which is still one gate.

**A completion test is not a checker node, and carrying both is not redundancy.** §6's
checker sits between a parallel layer and its convergence and judges *arriving siblings*
from the outside — including the two things no node can establish about itself: whether it
contradicts a neighbour, and whether it answered the question the layer was actually
asked. The completion test is the node's own gate on its own output, run by whoever owns
it. Drop the per-node test and the checker becomes the only gate in the graph, which is how
a convergence ends up re-deriving what each branch should have proved about itself; drop
the checker and every branch can pass its own test and still disagree with its neighbour.

**An edge is a dependency, and it carries data.** It exists when the second node
genuinely consumes what the first produced. Not when the second merely *happens after*
the first.

That distinction is the whole discipline. Write it on the edge and it stops being
abstract: `research --findings--> write --draft--> verify`. **An edge you cannot label
with what crosses it is not an edge.**

## 2. Your loop is already a graph, and most of its edges are fake

A prompt that says *"research this, then summarise, then draft"* is a graph — a single
unbranching chain in which every step waits for its predecessor. It is correct. It is
also the slowest possible arrangement of that work and the most brittle: one bad step
takes the whole chain, and nothing runs while any step is running.

The first move is therefore not to learn a new structure. It is to look at the one you
already have and ask which of its waits are real.

## 3. The fake-edge test

Five minutes, no tooling, and it is the highest-yield thing in this file —
and its question is **typed**, because "does data cross?" alone deletes real
constraints (AS-04). An edge is one of four kinds:

| Kind | What crosses | Example that MUST survive |
|---|---|---|
| **data** | A's output enters B | findings → draft |
| **control** | ordering only — a causal constraint with no bytes | backup → migration |
| **authorization** | a decision that permits B | approval → charge |
| **resource** | A and B touch one thing that tolerates one writer | two writes to one ledger |

1. Write every step as a box.
2. Draw an arrow between each pair of consecutive steps.
3. For each arrow ask, in order: does data from A enter B? does B's SAFETY
   depend on A having finished (control)? does A PERMIT B (authorization)?
   do A and B contend for one resource?
4. Any yes → keep it, **type it, and write the rationale on the arrow** —
   the payload for a data edge, the constraint for the other three.
5. No to all four → delete it. That wait encoded the order somebody typed.
6. Everything with no incoming arrow starts immediately — and **before any
   fan-out, compare the branches' side-effect footprints and read/write
   sets**: two read-only reviews genuinely parallelise; two writers of one
   file were a resource edge nobody drew.
7. Everything with no outgoing arrow is a final output.

The tell that the test is being done honestly is step 4: an arrow with no
type and no rationale is fake, and the person drawing it now has to say so
out loud rather than leaving it in place because it looked orderly. The
inverse tell is step 5 done lazily: backup→migration, approval→charge and
lease→edit all carry NO payload, and deleting them for that is how a
migration runs against nothing — an empty payload cell justifies deletion
only when there is also no causal, permissive or resource constraint.

**Expect two or three fake edges in any workflow you have not run this
against.** The classic is *"review file A, then review file B"*: it reads as
a sequence, the review of B never looks at what A returned, and both are
read-only — no data, no control, no authorization, no resource.

## 4. The diamond

One node fans out into several independent nodes; those all feed one node that combines
them. Drawn out, it is a diamond, and it is the shape that makes graphs worth the setup.

```
                 ┌──────────┐
                 │  SPLIT   │
                 └────┬─────┘
        ┌─────────────┼─────────────┐
        ▼             ▼             ▼          ← parallel layer
   ┌─────────┐   ┌─────────┐   ┌─────────┐
   │ source1 │   │ source2 │   │ source3 │
   └────┬────┘   └────┬────┘   └────┬────┘
        └─────────────┼─────────────┘
                      ▼
                 ┌──────────┐
                 │SYNTHESIZE│                  ← convergence
                 └──────────┘
```

The convergence waits for the slowest branch, not for the sum of all of them.

**Two rules, and both have to hold:**

1. **The parallel nodes are genuinely independent** — no fake edge dressed as a real one,
   and no shared mutable state. Two "independent" workers writing one file are one node
   with a race in it.
2. **The convergence genuinely needs all of them.** If it needs only the first to answer,
   the rest are paid-for waste and you wanted a race, not a diamond.

Once you look for it, the shape is everywhere there is a *gather-then-combine*: research,
multi-file review, market analysis, a fan of checks over one artifact.

## 5. Two ways a diamond fails silently

Both are failures **of the convergence**, which is why sequential chains do not have
them.

**A bad node goes undetected.** Three branches run, one returns a hallucination, an empty
result or a misread file, and that output arrives at the synthesis node beside two good
ones. The synthesis node does not know one of its inputs is wrong. It produces a
confident answer built partly on garbage. Parallelism bought the speed by deleting the
checkpoints where a human would have noticed.

**The error cascades and dilutes.** In a chain, a bad step produces a visibly bad output.
At a convergence, the bad output is *mixed* with good ones, so the damage is spread thin
and the trace back to its source is gone. By the time anything looks wrong, three nodes
have averaged it into plausibility.

Both are the same defect: **the convergence trusts its inputs because they arrived.**

## 6. The checker node

A node between the parallel layer and the convergence whose only job is to decide whether
each output may proceed. It synthesises nothing and writes nothing. It answers *is this
usable* and then passes, flags, retries or drops.

Six things it must catch — the list is the contract, and a checker that cannot say which
of the six it is asserting is not a checker:

<!-- checker-contract: missing, empty, unevidenced, malformed, contradictory, off-topic | optional: under-confident -->

1. **Missing** — a branch the split promised never came back at all. This item needs an
   **arrival count**: the fan-out is fixed when the layer is built, so the checker is
   handed the number of results to expect and compares it with the number it is holding.
   Without that number a vanished branch is caught only when its slot happens to hold
   nothing — item 2, by luck rather than by design, and only on a host that fills the slot
   at all (§9's primitive table records what this one does with a thrower). A host that
   simply drops it makes the gap invisible, and a short list looks like a complete one.
2. **Empty or null** — the result arrived and carries nothing usable.
3. **Unevidenced** — an assertion with no receipt: no citation, no tool result, no file
   and line, nothing a later reader could re-check. Whether the claim is *wrong* is item 5
   or 6's business; this item is about a claim nobody downstream can verify at all, and it
   is the one most checkers are missing.
4. **Malformed** — a shape that will break the convergence node's parsing.
5. **Contradictory** — two results that cannot both be true, **including two that used
   different words for one shared assumption**. The disagreement hides inside the
   paraphrase, so comparing strings is not enough to find it.
6. **Off-topic** — an output that answers a different question than the one asked.

Four of the six are code checks (1–4) and cost nothing; only 5 and 6 need a model. Run
them in that order — this is `agent-evals` §5's *cheap checks first*, applied to a
position in the graph rather than to a test suite.

**A confidence signal is optional, and it is not the evidence item.** *Under-confident*
was item 4 of this list until 2026-08-19; it is kept, demoted to a hint. A branch's own
number is worth carrying as exactly that — which result to retry first, which to hand to a
person — and it is not a gate. It never stands in for item 3, and the argument is this
pack's own, made twice already: *an uncalibrated judge is an opinion with a number
attached* (`agent-evals` §5), and for anything consequential the control is a
deterministic limit or a human, **never a classifier's confidence**
(`references/governance.md`). A gate that asks a branch how sure it is and never asks
what it can show has graded the run instead of measuring it. So: low confidence *flags*,
absent evidence *blocks*.

**What a checker costs, and how it turns into a rubber stamp — this pack's addition.** A
checker is a node, so it has the failure mode of every node: it can be wrong. A model
checker that has never been shown a bad input will pass everything, and a graph with a
checker that always says yes is strictly worse than one with no checker, because the
absent checkpoint has been replaced by a false one. So:

- **Give it a planted bad input at least once and watch it refuse.** Same rule as any
  other guard in this family.
- **Record its verdicts as scores with a source** (`agent-evals` §7), or you can never
  ask afterwards how often it fired.
- **A checker that has never rejected anything is a finding**, not a reassurance.

**Wire the convergence to the checker, not to the layer.** The synthesis node depends on
the checker; the checker depends on the branches. If synthesis also takes a direct edge
from a branch, the gate has a bypass and the shape is decoration.

## 7. Static or dynamic

A **static** graph has its nodes and edges decided before it runs. A **dynamic** graph
grows: a node finishes, looks at what it found, and decides what should come next.

| Reach for | When |
|---|---|
| **static** | the task repeats and the structure is the same each time |
| **static** | predictability and speed matter more than flexibility |
| **static** | **always first** — switch only after the static version hits a wall you can name |
| dynamic | the scope of the work depends on what is discovered along the way |
| dynamic | a node must choose its successors from its own output |
| prefer static | **you will need to audit exactly what ran and why** — but see below |

The last row is a PREFERENCE, not a hard ban — and the earlier draft got this wrong by
equating auditability with static structure, which conflates the plan drawn beforehand
with the execution graph saved afterward. **Auditability is a property of the RECORD, not
of the shape:** a run is auditable when its execution record is complete (every node,
edge and event that ran), the policy version is captured, and the run stayed inside
deterministic bounds — a **budget, a depth cap and a node cap**, each with provenance. A
static graph is preferred because its executed shape usually matches the drawn one and it
is predictable; a DYNAMIC graph under those caps, saving that record, passes the same
reconstruction audit — the SAME reconstruction that detects a deleted event or edge in
either mode. What never passes, static or dynamic, is a design DIAGRAM on its own:
*"here is the graph"* is not *"here is what happened"*. That is the same failure
`agent-evals` names when a system has no durable trace.

**Most workflows that feel like they need a dynamic graph need a better static one.**
Dynamic is more powerful and much harder to control; it is the second reach, never the
first.

## 8. When not to build a graph at all

The honest cost table. A graph is not free, and for a one-off it usually loses:

| | Chain | Graph |
|---|---|---|
| Time to build | low | higher — the dependencies have to be worked out |
| Time to run | the sum of the steps | the longest path |
| Debugging | easy — one line to walk | harder — concurrent state, diluted errors |
| Mid-run failure | poor, but visible immediately | good **only if** there is a checker |
| A one-off task | right answer | overkill |
| Something you run weekly | works | better, and the setup amortises |
| Growth in task size | does not scale | scales |

**Build the graph when the work repeats, or when a mid-run error is expensive enough that
the checker pays for itself.** Otherwise write the chain and move on — this is
`agent-harness`'s *start at the simplest thing that works* applied to shape.

## 9. What Claude Code actually executes

**This section is the pack's, not the source's, and it exists because the source's one
operational claim has since changed.** The article tells the reader that Claude Code has
a `workflow` keyword which parses a YAML block of `nodes:` and `depends_on:` and
parallelises it. Two corrections, both from the vendor's own changelog:

| Version | Entry (quoted from `anthropics/claude-code` `CHANGELOG.md`) |
|---|---|
| v2.1.154 | "Introducing dynamic workflows: ask Claude to create a workflow and it orchestrates work across tens to hundreds of agents in the background" |
| v2.1.160 | "Renamed the dynamic-workflow trigger keyword from `workflow` to `ultracode`. The word 'workflow' no longer triggers a run; asking for one in your own words still works" |
| v2.1.178 | the keyword "trigger[s] only on explicit phrases like 'run a workflow' or 'workflow:', not on any mention of the word" |
| v2.1.219 | dynamic workflows "default to a medium size guideline (aim for fewer than 15 agents)"; settable via `workflowSizeGuideline` |
| v2.1.229 | fan-outs "stagger same-prefix sibling agents so subsequent agents read the cached prompt prefix instead of re-paying it" |

So the keyword named in the article stopped being the keyword in v2.1.160, and the
opt-in today is `ultracode` or an explicit phrase.

**And the YAML is not what runs.** The host does not parse `nodes:`/`depends_on:`. It
authors and executes a **script** whose primitives are the real contract:

| Primitive | Is | Note |
|---|---|---|
| `agent(prompt, opts)` | one subagent | `opts.schema` forces a validated object back, so downstream stages get data, not prose to parse |
| `parallel(thunks)` | concurrent, **with a barrier** | awaits all; a thrower resolves to `null` rather than rejecting the call |
| `pipeline(items, ...stages)` | each item through all stages, **no barrier** | item A can be in stage 3 while B is still in stage 1 |
| `phase(title)` | a progress grouping | display, not semantics |
| `isolation: "worktree"` | a private checkout per agent | the only safe way to fan out writers |

Concurrency is capped at `min(16, cores − 2)` per run, and a run's total agents at 1000.
Passing 100 items is fine — they queue. **Those two numbers and the primitive table come
from the tool contract the host presents to the agent, not from the changelog** — a
different source with a different lifetime, which is why the stamp above names both. Read
them back from the running host before relying on them.

**Why this matters for the model above:** the article's diamond is `parallel()`, and the
next section is the distinction it does not draw.

## 10. Barrier or no barrier

**This section is the pack's.** A convergence node is a barrier: nothing downstream of it
starts until every branch has finished. The article treats that as the definition of a
diamond. It is actually a *choice*, and the wrong default.

A barrier is correct only when the downstream stage needs **cross-item** context:

- deduplicating or merging across the whole result set before expensive work;
- an early exit that depends on the total ("zero findings → skip verification");
- a stage whose prompt genuinely compares one item against the others — **which is
  exactly what a checker node does**, and is why the checker is a legitimate barrier.

A barrier is **not** justified by:

- *"I need to flatten or filter the results first"* — do that inside a stage;
- *"the stages are conceptually separate"* — separate is not the same as synchronised;
- *"it reads more cleanly"* — the cost is real. With five branches where the slowest takes
  three times the fastest, a barrier idles the four fast ones for two thirds of the wait.

The rule: **pipeline by default; barrier when a stage names the cross-item fact it
needs.** If it cannot name one, it does not need one.

## 11. Project defaults, written once

Anything you run more than twice deserves its graph conventions recorded where the agent
reads them (`CLAUDE.md`, or the equivalent for the host), so they are not re-derived per
session:

```markdown
## Workflow defaults

- A node with no declared dependency starts immediately; do not serialise by habit.
- Every declared dependency carries a type (data/control/authorization/resource)
  and a rationale. Delete an edge only when it has none of the four — an empty
  payload alone never justifies deletion: backup→migration carries no bytes and
  is real.
- Before a fan-out, compare the branches' side-effect footprints and read/write
  sets; writers of one resource serialise, read-only branches run in parallel.
- A checker sits between any parallel layer and the node that consumes it, and the
  consumer depends on the checker rather than on the layer.
- A checker flags; it never silently passes an incomplete output.
- A node that fails pauses the run and reports; nothing downstream consumes a flagged output.
- Outputs are files with the node's name; the graph passes paths, not transcripts.
```

The last line is `context-engineering.md`'s *filesystem as context* stated as a graph
rule: an edge that carries a path costs a few tokens, and an edge that carries a
transcript costs the window.

## 11a. MAST — the failures, named from outside

Everything above is argued from what breaks. *Why Do Multi-Agent LLM Systems Fail?* (2025)
measured it: execution traces from seven mainstream frameworks — MetaGPT, ChatDev, AG2 and
Magentic-One among them — with human annotators independently analysing roughly **150
traces** at **Cohen's kappa = 0.88**, producing **14 failure modes in three groups**.

The three groups land on this file's own node contract, which is the reason to carry them:

| MAST group | Modes include | The field it lands on |
|---|---|---|
| **System design flaws** | unclear interfaces between agents, overlapping roles, wrong tool configuration | `input` / `output` (the interface), and `owner` (overlapping roles) |
| **Inter-agent alignment failures** | inconsistent understanding of the objective, downstream misinterpretation, logically contradictory operations | the **edge payload** — what actually crosses, §3's *Carries* column |
| **Missing task verification** | an agent reports *completed* and the result does not meet the requirement | `check`, and §6's checker node |

**The load-bearing result is the negative one.** Straightforward fixes — better prompts,
more explicit role descriptions, retries — bought ChatDev only **15.6%**. The authors
conclude the modes are **architectural rather than bugs**, which is the same claim §1 makes
about the two fields nobody draws: an owner and a completion test are not documentation of
a graph, they are the parts that make it a graph.

Read the mapping as a review checklist rather than a taxonomy to admire: for each node, is
the interface stated, is there exactly one owner, and does the *check* run on something
other than the node's own claim of success.


## 12. The source's four diagrams, and what each one is for

The article carries four hand-drawn figures. They are not decoration — each one is doing
a specific job, and knowing which one saves re-reading the prose:

| Figure | What it shows | The job it does |
|---|---|---|
| **Cover — "Graph Engineering explained"** | `START` (define the task) → `SPLIT` (break into nodes) → a fan of three workers labelled *research / compare / check* → `CHECKER` (catch errors early) → `OUTPUT` (one clean answer) | The whole argument in one line, and the only figure in which the checker appears as a first-class stage rather than an afterthought |
| **Node / edge** | Three boxes — `Research` (in: topic, out: findings) → `Write` (in: findings, out: draft) → `Verify` (in: draft, out: final) — with `NODE` and `EDGE` labelled, and **the arrows themselves labelled with the data they carry** | Makes §1 concrete: the payload written on the arrow is what turns "comes after" into "depends on". This is the figure to copy when teaching the model |
| **The diamond** | One `RESEARCH` node fanning into `SOURCE 1/2/3` (bracketed *parallel layer*), all three converging on `SYNTHESIZE` | The ideal shape, drawn **before** the failure modes — deliberately without a checker, which is what §5 then attacks |
| **Workflow — how the code runs** | The same shape in code terms: `research_a/b/c` in a parallel layer, three arrows into `checker` annotated *waits for all three*, one arrow from `checker` into `compare` | The repaired shape. Its point is the single edge out of the checker: `compare` depends on the **gate**, not on the branches — §6's last paragraph, drawn |

## Where this file disagrees with its source

- **The keyword.** The source's `workflow` is `ultracode` since v2.1.160 (§9). Treated as
  a version-dated fact rather than a correction of the author: it was true when written.
- **The YAML.** The source presents `workflow:` / `nodes:` / `depends_on:` as a syntax the
  host parses. It is a way of *describing* a graph in a prompt, and it works for that; the
  execution contract is the script in §9.
- **The barrier.** The source's diamond always synchronises. This file makes the barrier a
  decision with a named justification (§10).
- **The checker's own reliability.** The source introduces the checker and stops. This
  file requires it to have been watched refusing a planted input, and treats a checker
  that has never rejected anything as a finding (§6).
