How I Compile a React Flow Graph So It Actually Runs
On IntegrateX, a React Flow canvas that looked finished was still entirely inert — a set of nodes and edges with positions. The interesting engineering question is the one the demos skip: how do you actually run it? The pretty graph was useless until we made it deterministic. A user-drawn graph has no inherent order, can fan out and then converge, and it may hide a cycle that will happily spin your workers forever. Turning that into a predictable, runnable pipeline is a compilation problem with a battle‑tested answer.
From render graph to execution plan
First, strip the canvas down to its essence. The executor does not care about positions or selection state; it cares about which capability feeds which. In the pattern I call Trinity Architecture, React Flow lives in the Presentation layer; the Reactive State / Orchestration layer (Zustand for IntegrateX) holds the active graph; and a Data / Serialization Adapter prepares what the runner needs. That adapter reduces rich UI state to a dependency map: for each node, the set of nodes that must finish before it can start. That adjacency — not the visual graph — is what you compile. A bonus from the same adapter on IntegrateX: we stripped UI‑only metadata and cut payload size by 94% before persisting.
Topological sort: the order falls out
A workflow graph is a DAG, and a DAG has a topological order: a linear sequence in which every node appears after all of its dependencies. Kahn's algorithm handles this cleanly. I let it pop nodes with no unmet deps, push successors when their indegree drops, and treat any leftovers as a hard error. That same pass becomes the cycle guard — if the processed count doesn't match, we fail fast before allocating a single worker or opening a stream.
// Kahn's algorithm: produces run order AND detects cycles
function compile(graph: RunGraph): Node[] {
const indeg = new Map(graph.nodes.map((n) => [n.id, 0]));
for (const e of graph.edges) indeg.set(e.target, indeg.get(e.target)! + 1);
const ready = graph.nodes.filter((n) => indeg.get(n.id) === 0);
const order: Node[] = [];
while (ready.length) {
const n = ready.shift()!;
order.push(n);
for (const m of successors(graph, n.id)) {
indeg.set(m, indeg.get(m)! - 1);
if (indeg.get(m) === 0) ready.push(byId(graph, m));
}
}
if (order.length !== graph.nodes.length) throw new Error("cycle detected");
return order;
}Parallelism is free once you have levels
The topological sort gives you more than a line — it gives you levels. Nodes that share the same dependency depth have no ordering constraint between them, which means they can run concurrently. In the diagram, A and B occupy step one and execute in parallel; C waits for both; D waits for C. Read concurrency from the structure and you avoid hand‑tuned queues, head‑of‑line blocking, and the kind of backpressure bugs I fought in streamerOS. Cap parallelism per level, stream results, and you won't thrash renders or starve downstream consumers.
Why compile at all
Compilation is the seam that makes everything downstream tractable. A compiled plan is cacheable, serialisable, and — crucially — verifiable before a single agent runs. You can reject invalid graphs, estimate cost, and replay a run deterministically because the order is fixed. It also enforces the Trinity boundary: UI edits don't leak DB schemas, and the adapter never mutates UI state — it feeds the orchestrator, which drives the runner. The visual graph is for humans; the compiled plan is for the machine, and keeping them separate is what keeps edits fast and execution reliable.
The canvas is how you think; the compiled plan is how you guarantee it runs. Topological sort does both jobs in one pass: it orders the work and proves the graph is runnable.
This is the runtime half of using React Flow as an orchestration canvas. For persisting these graphs without shipping the cache, see the serialization adapter pattern — the adapter layer in my Trinity split that made IntegrateX practical at scale. That seam — a visual builder compiled into a reliable runtime — is the judgment a team gets in an engineer who ships the compiler, not just the canvas.
Need an engineer who can build this?
I'm Yaseen Khatib — a Senior Full-Stack AI Engineer (MERN + TypeScript) who ships production AI systems solo. Open to senior and lead roles, remote or on-site.