Skip to content
echogeco
Workflows, state and the harness · Graphs

LangGraph state machines versus n8n graphs — one idea, two skins

Draw one SOP as nodes, edges and state, build it on a canvas or in code, mark where the two differ, and pick the skin by who tests and maintains it.

Lesson · 9 min · 19 of 75
On this page

A visual workflow tool and a graph library describe the same object. Steps are nodes, the allowed moves between steps are edges, what the run knows so far is state, and a saved copy of that state lets a run stop for a day and carry on. n8n draws the object on a canvas and stores it as JSON (Export and import (opens in a new tab)); LangGraph writes it in Python (Graph API overview (opens in a new tab)). Three of the six parts map one to one, one loosely and two only roughly, and the choice between the tools turns on who tests and maintains the graph, not on which demo looked better.

Running example: the recruiting screen

The running company in this course is a 14-person marketing agency. Each role it hires for draws about 150 applicants, and the founder has promised every applicant an answer within three days. The screen is a seven-step SOP:

  1. An application arrives: a form and a CV.
  2. Code checks three hard rules: the right to work, a portfolio link that opens, a salary expectation inside the band.
  3. The model scores the CV on a five-criterion rubric, 1 to 5 each, quoting a CV line as evidence for every score.
  4. Code routes on the total out of 25: 18 or more is invited, 12 to 17 goes to the founder, under 12 is declined.
  5. The founder approves or rejects each borderline applicant, in two sittings a day.
  6. Code sends an invite with a booking link, or a decline.
  7. The candidate books a slot; a reminder goes at 24 hours and the invite closes at 48.

Only step 3 has a model in it, and deterministic first is about why. Assume that of 150 applicants, 30 fail a rule, 24 score 18 or more, 36 land in the founder's band and 60 score under 12.

This drawing sends 90 declines, the 30 rule failures and the 60 low scores, straight from code with nobody deciding, because its job here is to compare two skins on a small graph. It is not the design to ship. A decline is a decision about a person, and the recruiting screen puts a named person's decision on every one before it is sent.

Three parts that map directly, and one loosely

A node is a unit of work that reads the state and returns what it changed: a Python function in LangGraph (Graph API overview (opens in a new tab)), a box on the canvas in n8n. An edge says "after this, that". A conditional edge picks its target from the state: a routing function that reads total and names the next node, or an If or Switch node with one output per branch. On the canvas the branch is a box of its own; in code it is a function attached to an edge.

State maps loosely. LangGraph's state is a declared schema, and each node returns an update to some of its fields. n8n passes items: each node receives the JSON its upstream node emitted (Understand n8n's data structure (opens in a new tab)) and can reference an earlier node's output by name (Reference previous nodes (opens in a new tab)). The screen's state is the same in both:

applicant_id    "role-14/app-0093"
rules           right_to_work, portfolio_ok, salary_in_band   (true or false each)
scores          five criteria, 1-5 each
evidence        one quoted CV line per criterion
total           5-25
route           invite | review | decline
decision        the founder's verdict and when                (empty until step 5)
invite_sent_at  timestamp                                     (empty until step 6)
booked_slot     timestamp                                     (empty until step 7)

In code that block is written once and the graph is compiled against it, so a test can start from any state you can write down. On the canvas it is a convention spread across nodes: nothing declares that total must exist before the Switch node reads it, so the check that it does is a node you add.

Each pill is where one applicant's thread can stand. The thread marked current is a saved state waiting for the founder, not a process.
100%SVG (opens in a new tab)

Pinch to zoom · drag to pan

Two parts that map only roughly

The checkpoint is where the tools part company. LangGraph's checkpointer saves the state after every step, keyed by a thread id (Checkpointers (opens in a new tab)); one applicant is one thread, and any thread's state can be read by its id while it waits. n8n's nearest equivalent is saved execution data (Understand executions (opens in a new tab)): what each node of a run received and emitted, stored so a person can inspect or re-run it. That is a record of a run, kept for debugging, not a state object other code reads by applicant. It is pruned by age and count, though a waiting execution is not (Manage execution data (opens in a new tab)).

The interrupt is the other. A LangGraph node calls interrupt(), the state is saved and the run stops; the founder's verdict resumes it on the same thread id a day later, with no process waiting in between (Interrupts (opens in a new tab)). n8n's nearest are the Wait node, which pauses an execution until a form or a webhook call arrives (Wait (opens in a new tab)), and review of a model's tool calls (Human-in-the-loop for tools (opens in a new tab)). Both stop a run and resume it. Neither gives other code a record per applicant that it can read and amend while the run waits; on the canvas the list of waiting applicants is the executions view filtered to waiting.

One detail decides how the screen is built: when a thread resumes, the interrupted node starts again from its first line, so anything it did before pausing happens twice. A node that sends the invite and then waits for the booking sends two invites. Split it into two nodes, or give the send an idempotency key so the second send finds the first.

PartLangGraph (opens in a new tab)n8n (opens in a new tab)Map
Nodea functiona box on the canvasdirect
Edgean edge between two functionsa connection between two boxesdirect
Conditional edgea routing functionan If or Switch nodedirect
Statea declared schema, updated by each nodeitems passed along, earlier output by nameloose
Checkpointsaved after every step, per threadexecution data, per execution, prunedapproximate
Interruptinterrupt(), resumed on the thread idthe Wait node; tool-call reviewapproximate

The same screen on a canvas

The n8n version is ten boxes, two of them Wait nodes (Wait (opens in a new tab)). The founder's verdict arrives as a form submission that resumes one waiting execution; up to 36 executions wait at once, each a row in the database rather than a running process.

The same branches at the same thresholds, and the same two waits. What differs is everything around the graph.
100%SVG (opens in a new tab)

Pinch to zoom · drag to pan

When the skin matters

The graph is identical in both drawings. Three questions decide the skin, and none of them is which tool can do more.

How is it tested? A LangGraph node is a function, so the boundary between 17 and 18 is a unit test: call the routing function with a total of 17, 18 and missing, and assert the branch on every change (Test (opens in a new tab)). In n8n, testing happens in the editor; pinned data replays a node's saved output while you build and is not used in production runs (Pin and mock data (opens in a new tab)). The same three cases are three manual runs after every change.

How is a change reviewed? n8n saves a workflow as JSON, and on the plans that include it, source control pushes it to a Git branch with a diff per workflow (Use source control and environments (opens in a new tab)). The JSON also records where each box sits, so tidying the layout is a diff too. In LangGraph, moving the founder's band to 13–17 is a one-line change in review, next to the test that covers it (Graph API overview (opens in a new tab)).

Who changes it in six months? The threshold will move. If the person making that change is an operations manager who reads a canvas and not Python, the canvas is the skin, and the loose rows of the table are covered by habit: a Code node that checks the state's fields before the Switch, and the three boundary runs after every edit. If the screen needs tested routes and state that other systems query, the code skin earns its place, with a developer on staff or on retainer. Either way a node is a contract, the idea in designing to an interface.

In practice

Ask whoever builds it for three things before the first demo: the state, field by field; where a waiting applicant's state lives and what happens to it when the server restarts; and the test, or the manual run, for an applicant who scores exactly 17. Then ask who makes the next change, and whether they can.

The mistake that costs money is choosing the skin by the demo. Both demos run the happy path. The bill arrives with the first change: a graph in Python nobody on staff can edit, so every threshold change is an outside invoice; or a canvas with no check that total exists, so a model node that stops emitting it breaks the routing without an error.

Check yourself

1. The operations manager, who does not write Python, will own the screen, and the founder's review can take up to a day. Which skin, and what do you add to cover the approximate rows?

The canvas, because its owner can change it; the founder's step is a Wait node resumed by a form, and a waiting execution is not pruned. Add a Code node before the Switch that stops the run if total is missing, write the applicant id into each execution's saved data so waiting applicants can be listed, and repeat three boundary runs, 17, 18 and missing, after every edit.

2. All 36 borderline applicants are scored after the founder's 16:00 sitting on day one. How many threads wait overnight for her 09:00 sitting, and what does holding them cost?

36, each a saved state of about 2 KB, roughly 72 KB in all, with no process running, so holding them costs nothing worth counting. The cost is the founder's sitting: 36 applicants at about 3 minutes each is 108 minutes, which at the assumed $100 an hour for the founder is $180 per role.

3. A contractor's node sends the invite and then calls interrupt() to wait for the booking. The candidate books the next morning. What happens, and what do you change?

The node starts again from its first line, and the candidate gets a second invite. Split it into two nodes, one that sends and one that waits, so the send is checkpointed before the wait begins, and key the send on the thread id plus "invite" so a rerun after a crash cannot send twice either.