A Node You Can't Describe Is a Node You Can't Route
Read12 min · Map → Contract → Route → Verify · Updated 2026-09-15
You end this lesson with: A written contract — job, input, structured output, named failure state — for every node in that map.
Chapters
- Four properties, no exceptions01
- When a node needs the full contract02
- Contracts make nodes swappable03
- Writing the contract before the prompt04
- Faded case05
- Open case06
Lecture 03. A Node You Can't Describe Is a Node You Can't Route
The audit from Lecture 02 gave a contract-review graph its real edges: fetch feeds four extract nodes, each extract feeds its matching check, all four check nodes feed write risk note. Wiring started immediately, and immediately broke.
The extract liability node returned a paragraph: "The contract caps liability at 12 months of fees, standard language, nothing unusual." The check liability node was supposed to read a clause and a number. Instead it got a paragraph and had to figure out, itself, what the clause said and whether a number was even in it. Half the time it got that right. The other half, it re-summarized an already-summarized summary and lost the number entirely.
Nothing was wrong with either node's reasoning. What was wrong was that extract liability's output had no shape. The next node couldn't route on it: it had to interpret it, and interpretation is another model call, another sample, another chance to be wrong about something that was already known one step earlier.
Four properties, no exceptions
Every node in a graph that survives contact with real work has four properties. Skip one and the node is not ready to wire up, no matter how good the underlying prompt is.
1. One job, small enough to name in three words. "Extract liability clause." Not "handle the liability section": that's two or three jobs wearing one name, and a node with more than one job can't be given one clean contract.
2. An explicit input. Not "the contract," but "the contract's raw text, plus the clause label to extract." If a node needs two things, both are declared. Nothing is assumed to be sitting in a shared conversation history.
3. A structured output. Not prose, but a shape the next node can read without asking a model what it means:
interface ExtractedClause {
clauseType: "liability" | "termination" | "data_handling" | "auto_renewal";
found: boolean;
text: string | null; // the clause's exact text, or null if not found
sourcePage: number | null;
}
check liability now reads found and text directly. No summarizing, no guessing whether a number is buried in a sentence somewhere.
4. A named failure state. Not an exception, but a value. found: false is the failure state for extract liability, sitting right there in the same shape as success. The graph routes on it exactly like any other field:
type CheckResult =
| { verdict: "pass" }
| { verdict: "fail"; reason: string }
| { verdict: "clause_not_found" }; // named failure — routes to re-extract, not a crash
That last property is the one people skip. When a node can return clause_not_found as data, the graph routes it: back to extract, to a human, wherever the design says. When a node throws instead, the graph stops, and someone has to notice the stack trace and figure out by hand what should have happened next.
Drawn as a graph, the routing looks like this:
The retry and the escalation are edges just like any other, chosen by reading found — not by catching an exception.
When a node needs the full contract
Use the full four-line contract when a node's output is read by another node in the graph — every node that lives past a single call. Skip the ceremony only for a one-off exploratory call whose output nothing else will ever read: that isn't a node yet, it's scratch work, and it doesn't earn a contract until you promote it into the graph.
| Node's output | Full contract needed? | Why |
|---|---|---|
Read by another node downstream (extract liability → check liability) | Yes | The next node routes on the shape; prose or a thrown exception blocks that routing |
| Consumed once, inside the same call, never stored or passed to another node | No | Nothing downstream depends on the shape holding |
| Expected to have its model, prompt, or implementation swapped later | Yes | The contract is what lets that swap happen without touching the node's neighbors |
The condition is observable in your own graph before you write a line of prompt: trace the node's output forward. If any other node reads it, or if you expect to rewrite what's behind this node later, write the four lines. If the output dies in the same call that produced it, it isn't wired into anything yet, and a contract for it can wait.
Contracts make nodes swappable
The other payoff of a real contract shows up later, not on day one. Once extract liability has a declared input and a declared output shape, you can change the model behind it, change its prompt, or replace the model entirely with a regex for boilerplate contracts, and as long as the output still matches ExtractedClause, nothing downstream needs to know or care. check liability was never written against a specific model's phrasing habits. It was written against a shape.
This is the same reason a well-designed function signature outlives its first implementation: the contract is the interface; the model call inside the node is one possible implementation of it, not the thing itself.
Writing the contract before the prompt
For every node the audit produced, write this down before writing a single line of prompt:
Node: extract liability
Job: pull the liability-cap clause out of one contract's text
Input: contract text (string), clause label ("liability")
Output: ExtractedClause { clauseType, found, text, sourcePage }
Failure states: found=false (clause absent) — routes to human review after 1 retry
Four lines. If you can't fill in all four without hedging, the node isn't scoped correctly yet: go back and split it, or merge it with a neighbor, before you write the prompt that implements it.
Faded case
Skip to the open case if this is familiar. The same graph has an extract termination node, sitting right next to extract liability, feeding check termination downstream. Its job: pull the termination clause out of one contract's text. Its input: contract text, clause label "termination". Its output can reuse the same ExtractedClause shape extract liability already declares, with clauseType set to "termination" instead. Decide the fourth line yourself: what should extract termination's failure state be, and where should the graph route when it fires?
Open case
Apply the same four-line format to a node in the task you contract in the exercises below — this time with no worked example to check your answer against.
Key takeaways
- A node with no shape forces the next node to interpret, not read. Interpretation is a model call you didn't budget for, and a source of errors you didn't design in on purpose.
- Four properties, always: one job, explicit input, structured output, named failure state. Missing any one of them means the node isn't ready to connect to the graph yet.
- A named failure is data the graph can route on. A thrown exception is not. It stops the run and asks a human to reconstruct what should have happened.
- Contracts make nodes swappable. Change the model, the prompt, or the whole implementation, as long as the shape holds, nothing downstream has to change.
What You Have Now
A written contract — job, input, structured output, named failure state — for every node in that map. Each one sits next to its node in your design document, ready to swap the implementation behind it without touching anything downstream. This is the Contract stage of the course's loop: the map from Lecture 02 turns into nodes a graph can actually route on.
Save it as graph/lecture-03.md; the lab's checker looks for that file at that path.
Exercises
- Write four contracts. For four nodes in the task you've been mapping, write the four-line contract format above: job, input, output shape, failure states. Where you can't fill a line in cleanly, that's a signal the node's scope is still wrong.
- Find a thrown exception. In a real agent workflow you run, find one place where a failure currently halts the run instead of returning a named failure value. Redesign its output type so that failure is a case in the same shape as success.
Check your understanding
Reading is free — your progress isn't being saved
Nothing you did on this page is recorded. Paid students get saved progress and verified completions, recorded quiz answers, the spaced-repetition review queue, labs, and a certificate for each course they finish.
30-day money-back guarantee. One-time payment — lifetime access and updates.