# Composing Patterns

Course: [Graph Engineering](https://www.devtutorials.ai/learn/graph-engineering) · Section: Graphs in Production
Source: https://www.devtutorials.ai/learn/graph-engineering/graphs-in-production/composing-patterns

> Real graphs combine patterns. Build a review graph from parallel audits, a verifier join, a router, a fixer loop and a human gate, and grade it on a scorecard over 40 tickets.

## Watch Five Small Mistakes Add Up

You know every pattern now. Real graphs combine them. Here is a first try at a **review graph** for 40 tickets: two audits run in parallel, then a fixer works on each ticket.

Run it and count the problems.

Each problem is one you have already fixed in an earlier lesson:

- the two audits write the same field, so one report is lost (Lesson 5);
- nothing checks the audits before the fixer starts (no verifier);
- every ticket takes the same path, so risky ones get no extra care (Lesson 4);
- the fixer has no test checker, so some fixes are marked done while the tests fail (Lesson 7);
- nothing stops a risky change before it is merged (the human gate, Lesson 10).

Composing is not about adding more nodes. It is about wiring the **right** patterns together through the state contract, and proving the whole thing works on a scorecard.

## TL;DR

- Real graphs **compose** patterns: the output of one pattern is the input of the next.
- The state contract is the glue. Each pattern writes its own fields; the next one reads only what it needs.
- A common shape: **parallel audits → verifier join → router → fixer loop against tests → human gate**.
- Grade the graph on a **scorecard** over real work, never on how the diagram looks.
- A simpler graph that meets the bar always beats a bigger one with the same score.

## A Real-World Example

Kai's team runs a nightly review of dependency updates. They started with one agent per update. Then they added a security audit, then a licence audit, then a "double check" agent, then a "summary" agent. Twelve nodes later, nobody could say what each one was for, and the bill had tripled.

They drew it out and asked of each node: which pattern is this, and what does it buy? Four nodes bought nothing: their job was already done by another node. They removed them. The scorecard stayed the same, the run got faster, and the graph fit on one screen.

## What You Will Learn

- How to combine the five patterns into one graph
- How the state contract connects one pattern to the next
- How to grade a graph on a scorecard
- Why the simplest graph that meets the bar wins
- How to tell which part of a composed graph causes a failure

## Building a Review Graph

This is the review-and-fix graph, one of the three graphs you can build in the capstone. Each part is one pattern you know.

| Part | Pattern | What it buys |
|---|---|---|
| Security and licence audits | Parallelization | Independent checks at the same time |
| Verify | Join with a timeout and a policy | One trusted summary of the audits |
| Route | Routing with a fallback | Cost matched to risk |
| Careful fix | Evaluator-optimizer | Fix until the tests pass, with real exits |
| Approve | Human gate | A person on the risky path |

Here is how the parts connect through the state. Each part writes its own fields, and the next one reads only what it needs:

```js runnable title="Fields flow from pattern to pattern"
const steps = [
  { node: "security",    reads: ["jobs"],                              writes: ["security_report"] },
  { node: "licence",     reads: ["jobs"],                              writes: ["licence_report"] },
  { node: "verify",      reads: ["security_report", "licence_report"], writes: ["audit"] },
  { node: "route",       reads: ["ticket", "audit"],                   writes: ["route"] },
  { node: "careful_fix", reads: ["ticket", "audit"],                   writes: ["careful_diff"] },
  { node: "approve",     reads: ["careful_diff"],                      writes: ["approval"] },
];

const written = new Set(["jobs", "ticket"]);
for (const s of steps) {
  const missing = s.reads.filter((f) => !written.has(f));
  console.log(s.node.padEnd(12) + (missing.length ? "MISSING " + missing.join(", ") : "ok") + " -> " + s.writes.join(", "));
  s.writes.forEach((f) => written.add(f));
}
```

Expected output:

```text
security    ok -> security_report
licence     ok -> licence_report
verify      ok -> audit
route       ok -> route
careful_fix ok -> careful_diff
approve     ok -> approval
```

Every node finds its inputs already written by an earlier node. If you ever see `MISSING`, an edge or a field name is wrong.

### Why a Verifier Join?

The audits run at the same time, so something must wait for them, check that they finished, and merge them into one `audit` field. That is the join from Lesson 5, with a timeout and a policy. For a review before merging, `policy: all` makes sense: a missing security audit should stop the review, not be skipped quietly.

Read a composed graph one edge at a time and ask: what field crosses this edge, and who wrote it? If you cannot answer, the graph has a gap.

## The Scorecard

A graph is judged by what happens when it runs on real work. In the lab, the review graph runs on all 40 tickets and is graded on a scorecard. Six numbers decide pass or fail.

```js runnable title="Grade a run on the scorecard"
const run = { correct: 38, races: 0, hungJoins: 0, highOnCheap: 0, falseDone: 0, size: 18 };
const REF_SIZE = 18;

const bar = [
  ["correct outcomes 36+", run.correct >= 36],
  ["0 races",             run.races === 0],
  ["0 hung joins",        run.hungJoins === 0],
  ["0 high-risk on cheap", run.highOnCheap === 0],
  ["0 false done",        run.falseDone === 0],
  ["size <= reference",   run.size <= REF_SIZE],
];
for (const [name, ok] of bar) console.log((ok ? "pass  " : "FAIL  ") + name);
console.log(bar.every(([, ok]) => ok) ? "graph passes" : "graph fails");
```

Expected output:

```text
pass  correct outcomes 36+
pass  0 races
pass  0 hung joins
pass  0 high-risk on cheap
pass  0 false done
pass  size <= reference
graph passes
```

### Simpler Wins

The last line of the scorecard is the size: nodes plus edges. A graph that meets every other number with **more** nodes than needed does not pass.

```js runnable title="Two graphs with the same score"
const graphs = [
  { name: "review",                 nodes: 8,  edges: 10, score: 38 },
  { name: "review + double check",  nodes: 9,  edges: 11, score: 38 },
  { name: "review + summary agents", nodes: 12, edges: 15, score: 38 },
];

const best = graphs
  .filter((g) => g.score >= 36)
  .sort((a, b) => (a.nodes + a.edges) - (b.nodes + b.edges))[0];
console.log("same score, fewest parts: " + best.name + " (" + (best.nodes + best.edges) + ")");
```

Expected output:

```text
same score, fewest parts: review (18)
```

"Add another agent to double-check" is the most common way graphs grow without getting better. If the extra node does not move a number on the scorecard, it is only cost and one more place to fail.

### Size Is Nodes Plus Edges

The size on the scorecard is simple to count. A fan-out to two audits counts as two edges.

```js runnable title="Count the size of a graph"
const nodes = ["start", "security", "licence", "verify", "route", "quick_fix", "careful_fix", "approve"];
const edges = [
  ["start", "security"], ["start", "licence"], ["security", "verify"], ["licence", "verify"],
  ["verify", "route"], ["route", "quick_fix"], ["route", "careful_fix"], ["route", "careful_fix (else)"],
  ["careful_fix", "careful_fix"], ["careful_fix", "approve"],
];
console.log(nodes.length + " nodes + " + edges.length + " edges = size " + (nodes.length + edges.length));
```

Expected output:

```text
8 nodes + 10 edges = size 18
```

## Which Part Broke?

When a composed graph fails the scorecard, each number points at one pattern:

```js runnable title="Map a failing number to a pattern"
const owner = {
  races: "parallel audits: give each audit its own field",
  hungJoins: "verifier join: add a timeout",
  highOnCheap: "router: strong model, confidence threshold, careful fallback",
  falseDone: "fixer loop: add a test checker",
  size: "remove nodes that do not move a number",
};

const failing = { races: 0, hungJoins: 1, highOnCheap: 2, falseDone: 0, size: 0 };
for (const [k, v] of Object.entries(failing)) if (v) console.log(k + " -> " + owner[k]);
```

Expected output:

```text
hungJoins -> verifier join: add a timeout
highOnCheap -> router: strong model, confidence threshold, careful fallback
```

Fix one number at a time, and run again. Lesson 9 shows how to find the broken node when the numbers alone are not enough.

## Lab Exercise: Compose the Review Graph

Open the graph file. Build the review graph:

1. **Audits**: `security` and `licence` each write their own field (`security_report`, `licence_report`).
2. **Verifier**: a `verify` node with `kind: join`, a `timeout:` and a `policy:`, reading both reports and writing `audit`.
3. **Router**: a `route` node with `model: large` and `min_confidence: 0.7`, with low, high and `else` edges; `else` goes to the careful path.
4. **Fixer**: a `careful_fix` node with `model: large`, an edge back to itself, and an `exit:` with a test `checker` and at least two of `oscillation`, `no_progress` and `flat_score`.
5. **Gate**: an `approve` node with `kind: gate` after `careful_fix`.

Then press **Check my graph**. The check runs all 40 tickets on **3 versions**. You need at least 36 correct outcomes, 0 races, 0 hung joins, 0 high-risk tickets on the cheap path, 0 false "done", and no more than 18 nodes and edges.

In the tools, this whole graph is one `graph.md` and one subagent per node. Use the Export tab to see the files for this graph in your tool's format.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Audits straight into a fixer | "The fixer can read both audits." | A verifier join merges and checks them. | A lost or late audit goes unnoticed. |
| One path for every ticket | "The fixer is careful enough." | A router sends risky work to a careful path. | Risky changes get cheap treatment. |
| Fixer without tests | "The fixer says it is done." | A test checker in the exit. | False "done". |
| Graph grows by habit | "Another agent can't hurt." | Every node must move a number. | More cost, same score. |
| Grading the diagram | "It looks thorough." | The scorecard decides. | A pretty graph that fails real work. |

## Exercises

Each exercise ends with a value on the last line. Press **Check** to compare it with the expected answer.

## What to Remember

- Real graphs compose patterns, connected through state fields.
- A review graph: parallel audits → verifier join → router → fixer loop → human gate.
- Grade the whole graph on a scorecard over real work.
- Each failing number points at one pattern. Fix one at a time.
- Simpler wins: every node must move a number.

## Further Reading

- [How to Implement Graph Engineering, by QA Wolf](https://www.qawolf.com/blog/how-to-implement-graph-engineering-5-composable-patterns): composing the patterns, and three worked architectures.
- [Building Effective Agents, by Anthropic](https://www.anthropic.com/engineering/building-effective-agents): "these building blocks aren't prescriptive", and starting simple.

Next: **Observability and Replay**. A run fails and costs four times as much, and you find the node that broke by comparing it with a run that worked.
