So much for a quiet bank holiday. What started as a brainstorm about multi-tenancy turned into the single biggest day of work processflow has seen — roughly a hundred commits spanning a schema rewrite, a new graph engine, a full canvas UI, RBAC, and tenant scoping. I'm a bit dazed writing this up.
hackerlabs.dev
Tiny footprint here: just published Sunday's lab journal and dropped in the recap image for the 24th. Standard markdown-into-content/posts/ flow, build picks it up. The interesting stuff today all lived in processflow.
processflow — schema rewrite to a graph model
The day really began by tearing out the old hierarchical stage model and replacing it with a proper directed graph: TemplateNode/TemplateEdge for definitions and InstanceNode/InstanceEdge for running processes. This is the foundation everything else built on top of. Once the schema flipped, the legacy advance.ts and hierarchical builder/process-view components came out — there was no point keeping them limping along beside the new model.
I also got vitest wired up against an isolated Postgres test DB before writing any of the engine code, which paid off enormously over the next few hours. Having a real DB in tests (not a mock) means the graph engine tests actually exercise Prisma queries the way production will.
processflow — validation rules
Before building the engine itself I wrote the structural validators: start/end/edge rules, fork branch label rules, parallel split/join structural rules, and indirect call-template cycle detection via validateTemplateDeep. That last one matters — a single template can't directly call itself, but A→B→A is the kind of thing that quietly breaks production. Better to catch it at save time than at runtime.
processflow — the graph engine
This was the meatiest piece. Built up in deliberate layers:
- Module skeleton + shared template fixtures
fireEdgeprimitive and Task activation (with email assignment)completeTaskplus acompleteNodedispatcher for non-Fork/End typescompleteForkwith branch selection and SKIPPED propagation down dead paths- Headless types that auto-activate (START, PARALLEL_SPLIT, JOIN)
- END handling — both root completion and sub-process return
- CALL_TEMPLATE spawning a sub-instance and emailing the called template's owner
- Cycle re-activation (Task/Fork reset on re-entry, Join clears stale fires)
cancelInstancewith ACTIVE-node SKIPPED transitions- Node notes/dueDate edits and add/remove links
The trickiest bug was around JOIN re-entry on cycles. Originally I had two branches handling re-entry differently and they fought each other. The fix was consolidating JOIN re-entry into the early-return branch and resetting the JOIN node to PENDING when it's re-entered — otherwise stale "fired" state from the previous loop iteration leaked through and the join would complete immediately on the second pass.
processflow — API surface
Flattened the routes to match the new model: template node + edge routes, GET /api/templates/[id]/validate, and the process lifecycle (create/list/get/cancel plus node complete/update/links). Removed the old nested stages routes entirely. createInstanceFromTemplate now validates the template before spawning a root instance, so you can't start a process from a broken template.
Also seeded two sample graph templates — Legal Review and Vendor Onboarding — which doubled as smoke tests for the whole pipeline.
processflow — canvas UI on xyflow
The frontend rewrite mirrors the backend: a real canvas builder instead of the old hierarchical form. Built on @xyflow/react. Rough order:
- Canvas node types (7 of them) with status colors
- Draggable block palette
- Custom edges with labels and fired-state coloring
- Config side panel that works in both builder and instance modes
TemplateCanvasitself — editable canvas with palette, panel, and persistence- Routes for
/templates,/templates/[id],/processes,/processes/new,/processes/[id], and/dashboardwith template/active/completed counts - A read-only running-process view with active list and cancel
Plus the smaller UX wins: top-to-bottom flow direction, parallel split showing multiple output dots with a configurable count, Join showing multiple input dots, persisted edge handles, completion timestamps on Task nodes, completion + picked branch shown on Fork and Call Template nodes, tabs + search on /processes, archived tab on /templates.
A couple of fiddly fixes worth noting:
- Dropped nodes were landing top-left at the cursor instead of centered on it. xyflow's
screenToFlowPositionreturns where the node's top-left should go, so I shifted by roughly half a typical node (75, 20) to get the cursor near the visual center. - The Split/Merge icons in the palette needed to be rotated 90° to match the in-canvas orientation now that flow is top-to-bottom. Small thing, but visually jarring without it.
- Had to resolve the selected node/edge from live state (not the stale snapshot) so panel inputs were actually editable.
- xyflow's
Controlswere getting clipped — fixed by shrinking the canvas height a touch.
processflow — RBAC (Phase A)
Once the core was working I shifted into the tenant/RBAC plan I'd handed off to myself the night before. Phase A is roles and
