# Nodes, Edges, Shared State

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

> Write a state contract: every field has one writer, every node reads only the fields it needs, and a checkpoint lets a crashed run resume.

## Watch One Node Overwrite Another

Here is a small graph that checks a fix before it is marked ready. Three nodes run one after another: **lint**, **tests**, then **report**.

In this run, lint **fails** and the tests **pass**. The fix should be blocked. Run it and watch the field called `status`.

Lint wrote `status: blocked`. Then the tests node wrote `status: ready` over it. The report node read the whole state, saw "ready", and passed a broken fix.

No node did anything wrong on its own. The **shape of the shared state** was wrong: two nodes were allowed to write the same field. In this lesson you write a **state contract** that makes this impossible.

## TL;DR

- A graph's **shared state** is a small, typed record that travels with the work.
- A **state contract** lists every field, its **type**, the **one node** that writes it, and the nodes that read it.
- **One writer per field.** Two writers means one result can silently replace the other.
- **Whitelisted reads.** Each node lists the fields it reads. No node reads everything (`*`).
- **Checkpoints.** Save the state after each node, so a crashed run **resumes** instead of starting again.

## A Real-World Example

Ravi's team has a graph that reviews pull requests. A lint node and a test node both set a field called `verdict`. Most days, both pass, so nobody notices.

One Friday, lint finds a secret key in the code and sets `verdict: reject`. Two seconds later the test node finishes and sets `verdict: approve`. The pull request is merged with the key inside.

The fix took one line. Lint writes `lint_result`. Tests write `test_result`. **Only** the review node reads both and writes `verdict`. Now the verdict has one owner, and it sees both results.

## What You Will Learn

- The kinds of node and the kinds of edge, as a graph file writes them
- How to write a state contract: type, writer and readers
- How to find a field with two writers, and fix it
- Why a node should read only the fields it needs
- How a checkpoint lets a crashed run resume

## The Three Parts of Every Graph

In Lesson 1 you met the three parts: **nodes**, **edges** and **shared state**. Now you write them down in a **graph file**. Every graph in this course is a plain YAML file with the same parts.

```yaml
graph: check-ticket
checkpoint: after_each_node
nodes:
  - { id: lint,   kind: function, reads: [diff], writes: [lint_result] }
  - { id: tests,  kind: function, reads: [diff], writes: [test_result] }
  - { id: report, kind: model,    reads: [lint_result, test_result], writes: [status, summary] }
edges:
  - { from: lint, to: tests }
  - { from: tests, to: report }
state:
  lint_result: { type: "pass | fail", writer: lint }
  test_result: { type: "pass | fail", writer: tests }
  status:      { type: "ready | blocked", writer: report }
```

Each node is **one line**. It has an `id`, a `kind`, the fields it `reads` and the fields it `writes`. Writing a node on one line means you rarely need to fix the spaces at the start of a line.

### Seven Kinds of Node

| Kind | What it is | Example |
|---|---|---|
| `function` | Plain code. No model. | Run the tests, check a schema |
| `model` | One prompt in, one answer out | Write a summary |
| `agent` | A full agent running its own loop | Fix a bug until the tests pass |
| `router` | Reads the item and picks the next edge | Send risky tickets one way |
| `join` | Waits for parallel branches and merges them | Merge three audit reports |
| `orchestrator` | Finds work at run time and starts workers | Find every old API call |
| `gate` | Stops and waits for a person | Approve a payment change |

Every `agent` node is a loop from the last course. It still needs a checker and a stop rule, which go under `exit:`.

### Four Kinds of Edge

```js runnable title="Read the kind of each edge"
const edges = [
  { from: "lint", to: "tests" },
  { from: "route", to: "careful_fix", when: "risk == high" },
  { from: "fixer", to: "fixer" },
  { from: "split", to: ["security", "licence", "size"] },
];

function kindOf(e) {
  if (Array.isArray(e.to)) return "fan-out";
  if (e.from === e.to) return "loop-back";
  if (e.when) return "conditional";
  return "fixed";
}

for (const e of edges) console.log(e.from + " -> " + e.to + " : " + kindOf(e));
```

Expected output:

```text
lint -> tests : fixed
route -> careful_fix : conditional
fixer -> fixer : loop-back
split -> security,licence,size : fan-out
```

A **fan-in** is the other end of a fan-out: several edges arriving at one `join` node. You meet it in Lesson 5.

## The State Contract

The `state:` part of the file is the **contract**. It says what each field is and who owns it.

A good contract follows two rules.

**Rule 1: every field has exactly one writer.** If two nodes write the same field, the one that runs last wins, and the other result is lost without any error.

```js runnable title="Find fields with more than one writer"
const nodes = [
  { id: "lint",   writes: ["lint_result", "status"] },
  { id: "tests",  writes: ["test_result", "status"] },
  { id: "report", writes: ["summary"] },
];

const writers = {};
for (const n of nodes) {
  for (const f of n.writes) (writers[f] = writers[f] || []).push(n.id);
}

for (const [field, who] of Object.entries(writers)) {
  console.log(field + ": " + who.join(", ") + (who.length > 1 ? "   <- two writers!" : ""));
}
```

Expected output:

```text
lint_result: lint
status: lint, tests   <- two writers!
test_result: tests
summary: report
```

**Rule 2: every node reads only what it needs.** A node that reads `*` gets the whole state, including fields it does not understand and results it should not trust. It also costs more tokens, because every field goes into its context.

```js runnable title="What a node sees: the whole state, or a whitelist"
const state = {
  ticket: "#7 empty list shows undefined",
  diff: "+12 -3 lines",
  lint_result: "fail",
  test_result: "pass",
  notes: "tried 4 ideas, 3 were wrong",
};

function view(reads) {
  if (reads.includes("*")) return state;
  return Object.fromEntries(reads.map((f) => [f, state[f]]));
}

const whole = view(["*"]);
const listed = view(["lint_result", "test_result"]);
console.log("reads *       -> " + Object.keys(whole).length + " fields: " + Object.keys(whole).join(", "));
console.log("reads a list  -> " + Object.keys(listed).length + " fields: " + Object.keys(listed).join(", "));
```

Expected output:

```text
reads *       -> 5 fields: ticket, diff, lint_result, test_result, notes
reads a list  -> 2 fields: lint_result, test_result
```

Writing a `writer:` next to each field in `state:` is optional in the file, but it is a good habit. It makes the owner of every field visible at a glance, and the lab warns you when the nodes disagree with it.

### Types Make Fields Checkable

A type says what a field may hold. `"pass | fail"` can only be one of two words. `"ready | blocked"` can only be one of two. A node that tries to write `"looks fine to me"` into a `"pass | fail"` field has done something wrong, and a check can catch it.

Plain `text` is fine for a summary a person reads. But any field another node **decides** with should have a small, exact type.

## Checkpoints: Resume, Don't Restart

A graph run can stop halfway: the laptop sleeps, the network drops, a machine restarts. Without a checkpoint, the run starts again from the first node, and you pay for those nodes twice.

A **checkpoint** saves the state after each node, in a file on disk. After a crash, the graph reads the file and continues from the last finished node. This is the same idea as the Loop course's state file, now for the whole graph.

```js runnable title="Resume from a checkpoint"
const order = ["lint", "tests", "report"];
const saved = { lint_result: "fail", test_result: "pass" }; // written before the crash

function resumeFrom(saved) {
  const done = { lint: "lint_result", tests: "test_result", report: "status" };
  return order.find((node) => !(done[node] in saved));
}

console.log("checkpoint has: " + Object.keys(saved).join(", "));
console.log("resume at: " + resumeFrom(saved));
console.log("nodes run twice: 0");
```

Expected output:

```text
checkpoint has: lint_result, test_result
resume at: report
nodes run twice: 0
```

A checkpoint is also how a graph survives a human gate. When a node waits a day for a person's approval, the state waits safely on disk, not in a chat window.

## See the Contract in the Lab

Open the **State** tab for any scenario. It shows every field as a table: its type, who writes it and who reads it. A field with two writers is shown in red. You can **edit** the contract there: pick the one writer, tick the readers, set the type. Every change is written to the graph file.

The **Canvas** tab shows the same graph as boxes and arrows. Add a node from the palette, select it and press **Connect** to draw an edge, or **Delete** to remove it. It works with a mouse, a touch screen or the keyboard (Tab to a node, Enter to select, C to connect). The canvas and the graph file are always the same graph.

### A Contract Check in Code

Here is the whole contract check in a few lines: one writer per field, and no node reading everything.

```js runnable title="Check a state contract"
const graph = [
  { id: "lint",   reads: ["diff"], writes: ["lint_result"] },
  { id: "tests",  reads: ["diff"], writes: ["test_result"] },
  { id: "report", reads: ["lint_result", "test_result"], writes: ["status", "summary"] },
];

const problems = [];
const owner = {};
for (const n of graph) {
  if (n.reads.includes("*")) problems.push(n.id + " reads everything");
  for (const f of n.writes) {
    if (owner[f]) problems.push(f + " has two writers: " + owner[f] + ", " + n.id);
    owner[f] = n.id;
  }
}
console.log(problems.length ? problems.join("\n") : "contract ok: " + Object.keys(owner).length + " fields, one writer each");
```

Expected output:

```text
contract ok: 4 fields, one writer each
```

## Lab Exercise: Write a Safe State Contract

Now fix the graph from the start of this lesson.

Open the graph file. Change it so that:

1. Every field has **one writer**. Only the report node writes `status`.
2. No node reads `*`. The report node reads `lint_result` and `test_result`.
3. The graph has `checkpoint: after_each_node` at the top.

You can make these changes in the Graph file, in the State tab, or both. Then press **Check my graph**. The check runs **3 versions**: lint fails, tests fail, and both pass. Each version crashes at a different node. Your graph must end with the right status every time, and resume after the crash.

Don't fix the overwrite by changing the order of the nodes. Whichever node runs last still wins, and next week someone adds a fourth node. One writer per field is the only fix that keeps working.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Two writers | "Both nodes know the status, so both can set it." | One node owns each field. Others write their own results. | The last writer wins, and a failure is lost. |
| Reading everything | "Give every node the whole state, just in case." | Each node lists the fields it needs. | Extra tokens, and nodes act on results that are not theirs. |
| Untyped decision fields | "`status` can just be text." | Small exact types: `"ready \| blocked"`. | A node writes "probably fine" and nothing can check it. |
| No checkpoint | "Runs are short, crashes are rare." | Save state after each node. | A crash halfway pays for the first half twice. |
| Fixing by order | "Put the important node last." | Fix the contract, not the order. | The next new node breaks it again. |

## Exercises

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

## What to Remember

- A graph file has nodes (one per line), edges and a `state:` contract.
- Every field has one writer. Two writers means the last one silently wins.
- Every node lists what it reads. Nobody reads `*`.
- Fields that other nodes decide with get small, exact types.
- A checkpoint saves state after each node, so a crash means resume, not restart.

## Further Reading

- [How to Implement Graph Engineering, by QA Wolf](https://www.qawolf.com/blog/how-to-implement-graph-engineering-5-composable-patterns): nodes, edges and shared state as the three parts of every graph.
- [Building Effective Agents, by Anthropic](https://www.anthropic.com/engineering/building-effective-agents): workflows as steps with clear inputs and outputs.
- The **State** lesson in the Loop Engineering course: the state file that this lesson's checkpoint grows from.

Next: **Prompt Chaining**. One agent investigates and writes in the same context, copies a dead end, and you split it into two nodes with a clean handoff.
