TLDR: If you're auto-importing nodes into a React Flow canvas, derive position from an index. Never accumulate per-node offsets — they compound into an unusable drift.
The Problem
A marketing automation client was walking away from a contract. And they were leaving behind an entire invisible system — Make (a visual automation platform) scenarios, GoHighLevel (a CRM + marketing tool, "GHL") workflows, tagging logic, lead routing — all documented only in the heads of the people who built it.
On day one without them, the next person opens a laptop and finds… nothing they can actually read.
So we built a visual handoff site where every SOP (standard operating procedure) lives as a node on a React Flow (the @xyflow/react node-graph diagramming library) canvas. Open it, zoom in, click any node, read the full procedure.
How the Flat-File Approach Works
Every node is a plain .md file. Title, description, step list — all frontmatter and markdown.
The canvas reads those files at build time, maps them to React Flow nodes[], and renders. No database schema, no CMS — just markdown plus a canvas.json position file that says where each node lives on screen.
That's the concept. Simple. Clean. I loved it.
The Wall
Then we got to the npm run import-make script — the tool that ingests Make blueprint exports and auto-generates the node stubs.
One blueprint. Two. Ten. Fine.
Bulk import. CHAOS.
The naive approach: walk the nodes in order, add a fixed offset to the previous node's X/Y.
posX += STEP_X
posY += STEP_Y
First 3 nodes? Looked great. After 15? Everything was stacked in a drifting diagonal, half the nodes invisible off-screen. Import 40 and you'd need to scroll a thousand pixels to the lower-right corner to find anything.
The compounding-offset trap.
Each step adds to the last. Small errors multiply. And with per-source-kind partial rebuilds (so you can re-import just one source without torching the whole canvas), a second run would compound again on top of the first run's already-drifted positions.
The Fix
Grid-pack. Derive position from the node's index — absolute, not accumulated.
const col = i % NUM_COLS
const row = Math.floor(i / NUM_COLS)
return {
x: col * COL_WIDTH + MARGIN,
y: row * ROW_HEIGHT + MARGIN,
}
i is the node's position in the sorted import array. That's it.
No state. No accumulation. No drift.
Every run produces the same layout. A partial rebuild doesn't shift anything. You can import 100 nodes and they fill a predictable grid you can actually navigate.
Why This Matters to Me
The compounding-offset trap isn't React Flow-specific — it'll bite you anywhere you're auto-placing things in sequence. And it's seductive because it looks right at small scale.
The fix is almost embarrassingly simple once you see it. Derive from index. Not from the previous node. That one rule makes bulk layout deterministic, idempotent, and debuggable.
The whole project was an exercise in making invisible systems visible. The irony of having the canvas itself go invisible — everything sliding off-screen — was not lost on me.