Nodes, Edges, Shared State
By the Syncfusion DevTutorials team · Last updated
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.
Two nodes write the same field
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.
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
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:
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.
Before: lint and tests both write status, so the last one wins. After: each check writes its own result, and one node decides the status.
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.
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:
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.
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:
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.
Without a checkpoint, a crash means starting again. With one, the run resumes after the last finished node.
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.
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:
checkpoint has: lint_result, test_result
resume at: report
nodes run twice: 0
A crash halfway, with no checkpoint
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.
Read the state contract as a table
A Contract Check in Code
Here is the whole contract check in a few lines: one writer per field, and no node reading everything.
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:
contract ok: 4 fields, one writer each
Lab Exercise: Write a Safe State Contract
Now fix the graph from the start of this lesson.
Write a safe state contract
Open the graph file. Change it so that:
- Every field has one writer. Only the report node writes
status. - No node reads
*. The report node readslint_resultandtest_result. - The graph has
checkpoint: after_each_nodeat 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: nodes, edges and shared state as the three parts of every graph.
- Building Effective Agents, by Anthropic: 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.