What LangGraph buys over a hand-wired agent pipeline, and the one-line reducer that stops two parallel branches from clobbering each other
A pipeline of agents is a plain for-loop until two of them have to run at once and write to the same state object, and then the loop either serialises work that never needed serialising or lets the two writes race. I wired three decision engines this way instead — a Game of Thrones strategy oracle, an intelligence-operations oracle, and a survey-analysis pipeline — each a state machine of eight to ten nodes over one shared typed state, with the parallel branches fanning out and converging on a single node, a one-line merge rule letting both write the same list without a collision, and a validation gate that halts the run before the expensive model step. What it does not buy you is a reason to reach for any of this when your pipeline is honestly a straight line.
16 min read
Contents
- Three engines that all wanted the same shape
- A for-loop is right until two steps stop needing each other
- The topology is data, sitting apart from the work
- The one-line merge rule that makes the branch safe
- A fork in the wiring, not a scatter of early returns
- Resuming an expensive run instead of restarting it
- The same pause that survives a crash can wait on a person
- The three engines, side by side
- Where this pattern shows up outside a game oracle
- The build, and what to wire in early
- References
The first time I added a second parallel branch to one of these pipelines, the run died before either branch's work was used. Two analysis steps had finished at the same moment and both tried to append a warning to the same list on the shared state, and the runtime stopped with an error about two updates arriving for one value at once. Nothing was wrong with either step. The problem was that I had asked two things to write the same place and never said how their writes should combine.
That error is the whole argument for the machinery in this post, in miniature. A pipeline of agents is easy to write as a straight loop — call the first step, hand its output to the second, and so on — right up until two steps have no reason to wait for each other and every reason to write back to the same object. At that point the loop forces you to pick between running them one after another for no reason, or running them together and hand-merging their results without dropping one. A graph runtime makes that a property of the wiring instead of a thing you remember to do by hand.
Three engines that all wanted the same shape
I have built the same skeleton three times, for three unrelated problems.
The first was a strategy oracle for a fictional political world — think great houses, shifting alliances, who moves against whom. It runs eight nodes: a step that assembles the board state, then two that branch off it in parallel — one scoring threats, one modelling coalitions — that converge on a step estimating what each actor believes, then a sequential run through an adversary model, a strategy solver, a simulator, and finally a narration step that turns the numbers into prose.
The second was an intelligence-operations oracle — a single operative moving through a hostile network, planning under uncertainty about who is compromised. It runs ten nodes with the same fan-out and fan-in: assemble state, branch into an intelligence-network analysis and a coalition analysis at the same time, merge them at a belief-estimation step, then a longer sequential spine of information-value scoring, an adversary model, a planner that reasons over a belief state rather than a known one, a critical-path scheduler, a Monte Carlo simulator, and narration.
The third has nothing to do with games. It is a survey-analysis pipeline that ingests three raw data files, inspects them with a model before touching them, cleans and de-duplicates cohorts, builds a lift table, assembles a schema, and then hits a validation gate. If the gate passes or passes-with-warnings, the run continues to a final assembly step. If it fails, the run stops there and the expensive step never executes.
Three different problems, one recurring structure: some steps must happen in order, some genuinely can happen at once, one shared object carries the state between them, and at least one point in the flow is a fork where the run should either continue or stop.
A for-loop is right until two steps stop needing each other
The honest counterfactual has to be stated before the framework earns its place, because for a genuinely linear pipeline the framework earns nothing. If your steps run strictly in order and each hands its result to the next, a plain loop is correct, and reaching for a graph runtime is pure ceremony. Do not do it.
The framework starts paying the moment two things are true at once. First, some steps have no dependency on each other — in the strategy oracle, scoring threats and modelling coalitions read the same board state and neither needs the other's output — so running them back to back wastes the wall-clock of the shorter one every single run. Second, those independent steps write back to the same shared state. That combination is where a hand-rolled solution gets subtle.
You can, of course, reach for raw concurrency primitives and gather the two independent steps yourself. The trouble is not launching them; it is landing their results. Two steps that both append to the same list of warnings, or both set a key on the same dictionary, will race — and the failure is not a loud crash but a dropped write that surfaces three steps later as a belief estimate computed from half its inputs. You end up writing the merge logic by hand, for every shared field, and getting it exactly right under concurrency, which is the class of bug people are worst at reviewing.
The ranked reasons the graph runtime is worth it, then, are these. Parallelism you declare rather than orchestrate comes first. Safe convergence of concurrent writes comes second, and is the one that actually bit me. Declarative routing — the ability to fork or stop mid-flow as a piece of wiring rather than a scatter of early returns — comes third. Resumability, being able to restart an expensive run from where it stopped rather than the top, comes fourth. None of those is about making the model smarter. Every one is about making the plumbing around it honest.
The topology is data, sitting apart from the work
The thing that makes the rest possible is that the shape of the pipeline is written down separately from what each step does. Each step is an ordinary function that reads the shared state and returns only the fields it changed. The wiring — which step feeds which, where the flow forks, where two branches rejoin — is declared on its own, as a list of connections between named steps.
That separation is why the whole flow fits on one screen and reads like a map:
Two connections leave the state-assembly step, so its two successors run as a parallel branch. Both successors connect to the belief step, so the belief step does not begin until both have landed — the convergence point is an automatic barrier, not a countdown I maintain. Adding a third parallel analysis later is one more connection out and one more connection in. The step functions never learn that the topology changed, because they were never told the topology in the first place.
The one-line merge rule that makes the branch safe
Now the field where the two branches collide. When two steps in the same parallel
tick return a value for the same key, the runtime by default refuses to guess how
to combine them and raises what its documentation names an
INVALID_CONCURRENT_GRAPH_UPDATE —
which is precisely the error that opened this post. The default is a refusal, and
the refusal is a feature: it will not silently pick a winner.
The fix is to attach a merge rule to that field once, at the point where the shared state is defined. The rule for the shared error and warning lists is the simplest one there is — combine by appending, so two concurrent writes become one longer list instead of one overwriting the other. The docs call these reducers, and the one property they must have is associativity: merging A then B must equal merging B then A's contents into the batch, which is what lets the runtime replay a batch of writes without changing the result.
The before and after is stark, and worth seeing as two states rather than a description:
One line of declaration at the point the field is defined, and the entire class of dropped-concurrent-write bugs is gone from every branch that touches that field — not fixed case by case, but made unrepresentable. This is the reducer of the title, and it is the single highest-leverage thing the runtime gave me. The one caveat worth stating plainly: an append rule combined with a step that resends its whole list on each pass will duplicate entries, so the rule has to match how its field is written, not just what type it holds.
A fork in the wiring, not a scatter of early returns
The survey pipeline has a validation gate two-thirds of the way through, and it is the clearest example of the third advantage. After the schema is assembled, a gate step returns a verdict — pass, pass-with-warnings, or fail. The wiring reads that verdict and sends the run one of two ways: onward to the final and most expensive assembly step, or straight to the end.
The point is where that decision lives. It is one labelled fork in the topology, sitting next to every other connection, visible in the same map as the rest of the flow. The alternative — a check-and-return at the top of the expensive step, and another inside it, and a guard three steps earlier — spreads the same control flow across the very functions it is meant to protect, where no one reading any one of them can see the whole rule. Both oracles use the same fork for a different purpose: skip the narration step entirely if an earlier step recorded a fatal error, because there is no point paying a model to narrate a broken run.
Resuming an expensive run instead of restarting it
The fourth advantage only matters once a run gets long enough to hurt. Because the shared state is a single well-defined object and the runtime knows the boundaries between steps, it can snapshot that state after each step. The survey pipeline turns this on: its state is checkpointed as it goes, so a run interrupted at the gate can resume from the gate rather than re-ingesting and re-cleaning three files from scratch.
For the intelligence oracle this stopped being a nicety and became the plan for scale. That engine can be asked to reason over many turning points in sequence, each one an expensive planning problem. The intended design checkpoints after each turning point, so a run that dies on point nine of twelve restarts at nine. Without the snapshot boundary the runtime already maintains, that resume logic is something you build and get wrong; with it, it is a flag. I will be honest that the multi-point resume is designed and not yet something I have run end to end at length — the single-run checkpointing is what is live today, and the way to know the resume path works is to kill a long run halfway on purpose and confirm it comes back at the right point rather than the top.
The same pause that survives a crash can wait on a person
There is a second thing the snapshot boundary buys, and it is the one worth wiring in even when runs are short. A checkpoint does not only survive a crash; it lets a run stop on purpose and wait. The survey pipeline's gate decides pass, warn, or fail on its own today. Replace that automated verdict with a person's, and nothing else in the wiring moves — the fork is already there, the snapshot is already taken, so the run can sit at that node for an hour or a week until someone approves, then resume exactly where it paused instead of starting the ingest over.
That is the honest way to put a human in front of a step you cannot take back — a report that goes to a client, a charge, anything published. It is not a new subsystem. It is the fork and the checkpoint the pipeline already has, with a person reading the verdict where a rule read it before. Keep it risk-based: a hold on every harmless step only makes the pipeline slow, so gate the irreversible nodes and let the rest run.
The three engines, side by side
| Engine | Steps | Parallel branch | The fork does |
|---|---|---|---|
| Strategy oracle | 8 | threat + coalition → belief | skip narration on fatal error |
| Intelligence oracle | 10 | intel-network + coalition → belief | skip narration on fatal error |
| Survey pipeline | 8 | none — strictly sequential | stop before final step on a failed gate |
The survey pipeline is the useful counter-example in my own portfolio: it has no parallel branch at all, and it still earns the runtime — not for fan-out it never does, but for the gate and the checkpointing. That is worth sitting with. Three of the four advantages are independent of parallelism. If the only one you need is the first, and your flow is a line, you do not need any of this.
Where this pattern shows up outside a game oracle
Strip away the great houses and the operative, and the shape is a directed graph of steps over one shared record, with parallel branches that must merge cleanly, forks that stop bad runs early, and snapshots that let long runs resume. That description fits a great deal of work that has nothing to do with the examples here.
The unit is not the agent. It is the shared state, the rules for merging concurrent writes into it, and the forks that decide whether the run goes on. Get those three right and the individual steps become boring, which is what you want them to be.
| Setting | The parallel branch | The fork that stops the run |
|---|---|---|
| Loan underwriting | credit pull and income verification at once | decline before ordering a costly appraisal |
| Medical intake | labs and imaging read in parallel | halt before a specialist referral if a red flag fires |
| Ad-campaign build | creative checks and audience checks together | block launch if brand-safety fails |
| Insurance claims | fraud signals and coverage lookup concurrently | route to a human before auto-approving a payout |
| Content moderation | policy classifiers running side by side | stop before publish on any hard violation |
| ETL with validation | independent source pulls in parallel | quarantine the batch before it hits the warehouse |
Three design choices from these builds transfer directly, whatever the domain.
Declare the merge rule for a shared field the day you add the second writer, not the day it breaks. The concurrent-write refusal is a gift precisely because it fails loudly at wiring time rather than quietly at run time. Treat any state field that more than one step can write as needing an explicit merge rule, and write that rule when you add the branch, not after a dropped write has cost you a debugging afternoon three steps downstream.
Put every fork in the wiring, never inside the step it guards. A run that should stop should stop at a labelled branch you can see on the map, not at a guard clause buried in the function that would otherwise do the expensive work. The test is whether someone can read the topology alone and know every place the run can end. If they have to open the step functions to find the exits, the control flow is in the wrong place.
Snapshot at the step boundary before you think you need to. Checkpointing is cheap to turn on at the start and expensive to retrofit, because retrofitting it means reconstructing where your step boundaries even were. Any pipeline whose full run costs real money or real minutes should be checkpointed from its first version, so that the first time a run dies at step nine you resume at nine instead of rediscovering that you cannot.
The build, and what to wire in early
The reference shape, with the real pieces named. The runtime is LangGraph, and the shared state is a single typed dictionary that every step reads and writes. Each step is a plain function returning only the fields it changed. Independent steps are declared as parallel branches by giving one step two successors, and they converge by pointing both at a common next step. Any field two steps can write carries a merge rule — an append for the shared error and warning lists. Forks are labelled conditional connections that route on a verdict or stop on a fatal error. Long runs are checkpointed at the step boundary the runtime already knows. The heavy computation inside the steps is ordinary and deterministic — graph analysis, a planner over a belief state, a Monte Carlo simulator, a critical-path scheduler — and a language model is used only at the very end, to narrate results the pipeline already computed, never to decide them.
Three things are cheap to build in now and expensive to add later. The merge rule on every shared field, because adding it after the fact means auditing every branch that ever wrote there. The checkpoint boundary, because adding it later means rediscovering where your steps begin and end — and because it is also what lets a fork pause for a human instead of only surviving a crash. And the discipline of steps that return only what they changed, because a step that rewrites the whole state defeats both the merge rules and the snapshots at once. Wire those three in on the first pass and the rest of the graph stays boring for as long as you own it.
References
Tooling
- LangGraph — state, schemas and reducers — the reference for annotating a state field with a merge rule and the associativity requirement on it.
- LangGraph —
INVALID_CONCURRENT_GRAPH_UPDATE— the concurrent-write refusal that opens this post, and the reducer that resolves it.