# Prompt Chaining

Course: [Graph Engineering](https://www.devtutorials.ai/learn/graph-engineering) · Section: Graph Foundations
Source: https://www.devtutorials.ai/learn/graph-engineering/graph-foundations/prompt-chaining

> Split one fat node into investigate, then write. The writer starts clean with a typed handoff, never sees the dead ends, and gets the answer right.

## Watch an Agent Copy Its Own Dead End

A bug report says: "Login fails after a password reset." One agent gets the job: find the cause, then write a short script that shows the bug.

It tries six ideas. Five are dead ends. Then, in the same conversation, it writes the script. Run it and read the last line.

The script tests one of the **dead ends**, not the real cause. The agent found the right answer, but by the time it wrote the script, its context held six ideas, and the wrong ones were long and convincing. It copied one.

This is the oldest graph pattern of all: **prompt chaining**. Split the job into two steps, and let the second step start clean.

## TL;DR

- **Prompt chaining** splits one job into steps. Each step is a node. Each one passes a **handoff** to the next.
- What it buys: **context isolation**. The next step does not see the last step's dead ends.
- The handoff should be **typed**: a small, exact shape like `{cause, steps, file}`, not a long message.
- Measure it: the writer's context should be a **small fraction** of the one-node version.
- It fails when each step forwards everything. Then it is one node with extra waiting.

## A Real-World Example

Lena asks her agent to migrate a page to a new design system and then write release notes. The agent spends an hour on the page. It tries three layouts, breaks the build twice and fixes it.

Then it writes the release notes. They mention a layout it threw away and a bug it already fixed. Every dead end from the hour of work is still in its context.

Lena splits the job. The first agent migrates the page and writes a short handoff: what changed, which files, which tests. A second agent writes the release notes from **only** that handoff. The notes are now short and right.

## What You Will Learn

- What context isolation is, and why dead ends hurt the next step
- How to split one fat node into two, with one handoff
- How to type a handoff so the next node knows exactly what it gets
- How to measure a node's context
- When chaining does not help

## Context Isolation

Everything a node reads goes into its **context**: the text the model sees before it answers. A model cannot ignore part of its context. If the dead ends are there, they compete with the right answer.

In a chain, the first node does the messy work. It writes a short result into the state. The second node reads **only** that result. The dead ends stay behind.

```js runnable title="Context: one fat node vs a chain"
const size = { report: 800, notes: 3600, findings: 400 };
const tokensFor = (reads) => reads.reduce((sum, f) => sum + size[f], 0);

const fat = tokensFor(["report", "notes"]);
const writer = tokensFor(["findings"]);

console.log("one fat node:  " + fat + " tokens");
console.log("chain writer:  " + writer + " tokens");
console.log("writer is " + Math.round((writer / fat) * 100) + "% of the fat node");
```

Expected output:

```text
one fat node:  4400 tokens
chain writer:  400 tokens
writer is 9% of the fat node
```

The writer's context is a small fraction of the original. That is less to pay for, and, more important, nothing wrong to copy.

### How the Dead End Gets Copied

```js runnable title="A model picks the idea it sees most"
const ideas = [
  { idea: "the cache is stale",        lines: 40, right: false },
  { idea: "the clock is in UTC",       lines: 25, right: false },
  { idea: "the reset token is reused", lines: 6,  right: true  },
];

// A rough model: the longest, most-discussed idea wins the writer's attention.
const loudest = (list) => list.slice().sort((a, b) => b.lines - a.lines)[0];

console.log("fat node writes about:   " + loudest(ideas).idea);
const findings = ideas.filter((i) => i.right);
console.log("chain writer writes about: " + loudest(findings).idea);
```

Expected output:

```text
fat node writes about:   the cache is stale
chain writer writes about: the reset token is reused
```

This is a cartoon of how models behave, not an exact rule. But the effect is real: a long, confident dead end in the context pulls the answer toward it.

## The Handoff

The edge between the two nodes carries a **handoff**: the one field the first node writes and the second node reads. Its quality decides the chain's quality.

A **typed** handoff has a fixed shape. In the graph file, you give the field a type in `state:`:

```yaml
state:
  findings: { type: "{cause, steps, file}", writer: investigate }
```

Now the writer knows it gets three things: the cause, the steps to show it, and the file. Nothing else.

```js runnable title="Check a handoff before passing it on"
function checkHandoff(findings) {
  const need = ["cause", "steps", "file"];
  const missing = need.filter((k) => !(k in findings));
  return missing.length ? "rejected, missing: " + missing.join(", ") : "accepted";
}

console.log(checkHandoff({ cause: "token reused", steps: 3, file: "auth/reset.ts" }));
console.log(checkHandoff({ cause: "token reused" }));
console.log(checkHandoff({ text: "I think it's the token, or maybe the cache" }));
```

Expected output:

```text
accepted
rejected, missing: steps, file
rejected, missing: cause, steps, file
```

A typed handoff also helps you. If a chain gives a wrong answer, read the handoff. It shows which side broke: the investigator found the wrong cause, or the writer misused the right one.

## Build the Chain

Here is the split as a graph file. The investigator writes its long `notes` **and** a short `findings`. The writer reads only `findings`.

```yaml
nodes:
  - { id: investigate, kind: agent, reads: [report], writes: [notes, findings], exit: { max_iterations: 8 } }
  - { id: write, kind: agent, reads: [findings], writes: [script], exit: { max_iterations: 4 } }
edges:
  - { from: investigate, to: write }
```

The notes are still useful: you can read them, and the run log keeps them. They just do not go to the writer.

### A Split That Does Nothing

Splitting is not enough by itself. Watch what happens when the writer reads the notes too.

Two nodes, two agents, and the same wrong answer. This is the failure the QA Wolf guide warns about: if every node forwards the full state, the chain is one node with extra waiting.

```js runnable title="Does this chain isolate anything?"
const original = 800 + 3600;
const chains = {
  "writer reads findings":         ["findings"],
  "writer reads findings + notes": ["findings", "notes"],
  "writer reads report + findings": ["report", "findings"],
};
const size = { report: 800, notes: 3600, findings: 400 };

for (const [name, reads] of Object.entries(chains)) {
  const ctx = reads.reduce((s, f) => s + size[f], 0);
  const pct = Math.round((ctx / original) * 100);
  console.log(name + ": " + pct + "% " + (pct <= 25 ? "(isolated)" : "(not isolated)"));
}
```

Expected output:

```text
writer reads findings: 9% (isolated)
writer reads findings + notes: 91% (not isolated)
writer reads report + findings: 27% (not isolated)
```

### Score the Script

The check in the lab asks one question about the script: does it test the real cause? Here is that check for three designs.

```js runnable title="Does the script test the real cause?"
const realCause = "the reset token is reused";
const designs = [
  { name: "one fat node",          writerSees: ["the cache is stale", realCause] },
  { name: "leaky split",           writerSees: ["the cache is stale", realCause] },
  { name: "chain with findings",   writerSees: [realCause] },
];

// The writer tests the first idea it sees: the loudest one.
for (const d of designs) {
  const tested = d.writerSees[0];
  console.log(d.name + ": tests \"" + tested + "\" -> " + (tested === realCause ? "right" : "wrong"));
}
```

Expected output:

```text
one fat node: tests "the cache is stale" -> wrong
leaky split: tests "the cache is stale" -> wrong
chain with findings: tests "the reset token is reused" -> right
```

## When Chaining Does Not Help

Chaining buys clean context. It costs a handoff and some waiting. Skip it when:

- The first step leaves **no dead ends**: the output of step 1 is already exactly what step 2 needs.
- The steps are so **coupled** that step 2 needs nearly everything step 1 saw. Then you would forward the whole state anyway.
- The job is a **strict sequence** one agent already does well. Lesson 1 showed that splitting it loses detail.

In Cursor, each subagent runs with its own context, and subagents in the other tools are separate conversations too. That is context isolation built in. A chain of two subagents, where the main agent passes only a short result between them, is exactly this pattern.

## Lab Exercise: Split Into Investigate → Write

Now split the fat node yourself.

Open the graph file. Change it so that:

1. An `investigate` node reads `report` and writes `notes` and `findings`.
2. A `write` node reads **only** `findings` and writes `script`.
3. An edge goes from `investigate` to `write`.
4. `findings` has a typed shape in `state:`, for example `{ type: "{cause, steps, file}" }`.

Then press **Check my graph**. Each of the **3 versions** has a different tempting dead end. The writer's context must be 25% or less of the one-node version, and the script must test the real cause every time. The check also runs **10 bug reports** per version: accuracy must rise to at least 8 of 10 (one fat node gets about 4).

Remove the edge from the old node back to itself. A loop-back belongs inside a node that needs retries, not around the whole chain.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| One fat node | "The agent knows which ideas were wrong." | Dead ends in context compete with the right answer. | The output copies a dead end. |
| Leaky split | "Two nodes means isolation." | Only what the next node reads matters. | Same context, same mistake, more waiting. |
| Untyped handoff | "A paragraph of findings is enough." | A small fixed shape: cause, steps, file. | The next node guesses what matters. |
| Throwing notes away | "Only the findings matter." | Keep notes in the state for people and logs. | Nobody can tell how the cause was found. |
| Chaining everything | "More steps are cleaner." | Chain only where context really should reset. | Each hop adds waiting and loses detail. |

## Exercises

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

## What to Remember

- Prompt chaining splits a job into nodes so each step starts with clean context.
- Only what the next node reads matters. A split that forwards everything isolates nothing.
- Pass one small, typed handoff along the edge.
- Keep the long notes in the state for people, not for the next node.
- Skip chaining when the steps need each other's full detail.

## Further Reading

- [How to Implement Graph Engineering, by QA Wolf](https://www.qawolf.com/blog/how-to-implement-graph-engineering-5-composable-patterns): prompt chaining, and when to skip it.
- [Building Effective Agents, by Anthropic](https://www.anthropic.com/engineering/building-effective-agents): prompt chaining as the first workflow pattern, with gates between steps.
- The **Context Engineering** course on this site: why what a model sees decides what it answers.

Next: **Routing**. A small router sends a payment change down the cheap path, and you give it a safe fallback.
