
Flowgraph: a flow engine built spec-driven
I've spent years building dynamic survey systems. The last one was the Survey App for Naiz Fit: offline-first, Kotlin Multiplatform, complex branching on the client, and every campaign provisioned as a single JSON. It worked well. And yet its post-mortem left several decisions on the table that, in hindsight, were debt.
Flowgraph is the from-scratch rewrite. The place where I'm testing how far you can go building a flow engine working spec-driven, with an agent implementing against the specification. A TypeScript monorepo with three packages —flow-core, flow-session, flow-react— plus a navigable demo. Everything I describe here is in the repo: in its specs, its constitution, and its tests.
The design comes from a post-mortem
The engine spec says it plainly: the design derives from the post-mortem of a previous-generation survey engine. The central lesson was that graph defects were silent at runtime. A poorly defined survey didn't fail when published; it failed at a tester's home, with no coverage and no one watching. Add mutable state that is impossible to audit and logic glued to the platform, and you have the full list of wounds.
Three bets come out of that:
- The flow is a serializable graph, not a tree with duplicated branches. Shared pages are defined once and paths reconverge.
- State is an event log, not a snapshot. Five types in v1 —
SESSION_STARTED,ANSWERED,ADVANCED,WENT_BACK,SESSION_FINISHED—, each event with its provenance (human, agent, or import) and the schema's SHA-256 hash recorded at startup. State is rebuilt withreplay: there is no other hydration path. - All the logic lives in a pure core: total functions
(schema, state, command) → Result, no DOM, no clock, no randomness, no IO.
Semantics pinned down by specification
Two decisions set this engine apart from everything I had built before.
The first is three-valued logic (Kleene): if a guard cannot be evaluated with the available data, the result is unknown and the edge does not fire. The system fails by asking for more information, never by fabricating a conclusion. The «concluding without data» class of bug —textbook in the previous engine— cannot be reproduced here, and there is a test that proves it.
The second is active truth: an answer only counts if its page is on the current path. If you advance down branch A, go back, and choose B, your answers from A stay in the log but stop counting; if you return, they reappear pre-filled when you re-enter through their page. The log keeps every fact; the interpretation is derived.
The constitution and mechanical enforcement
The project is governed by a 12-rule constitution —versioned, like the software— and seven numbered features in specs/: the engine, repeatable subflows, the MCP adapter, the React adapter, the LLM authoring loop, and the demos. Code that contradicts a spec loses.
What matters is that compliance does not depend on discipline:
flow-corecompiles with"lib": ["ES2022"]: DOM types simply do not exist in there.- ESLint bans
Date,Math.random,cryptoorfetchin the core. dependency-cruiserwatches the physical boundaries between packages: an illegal import does not compile.
flow-session, the only piece allowed to mutate, is under 100 lines. The adapters —React today, MCP tomorrow— are subscribers: they contain no business logic.
What is already built
The core is implemented and verified: decide/apply/replay, check structural, probe for bounded exploration, and goldens with edge coverage measured by the engine itself —not declared by whoever writes the test—. The numbers from the latest validation:
- 198 green tests and 100% coverage —statements, branches, functions, and lines— in core and session.
- React adapter with
useSyncExternalStore, renderers, and browser persistence. - A navigable demo: a fictional survey for psychology patients —three starting branches: sleep, stress, a major change— chosen because branching, conditional visibility, going back, reconvergence, restoration, and immutable completion are easy to grasp in that context. 14 Playwright tests on desktop and mobile Chromium, zero serious or critical accessibility violations, 97 KiB of compressed JS.
The demo's goldens traverse 100% of the graph's transitions: 14 out of 14.
What is being validated
Three pieces have a spec but no code yet, and they are the interesting ones:
- MCP adapter (spec 003): the engine exposed as tools for agents. The inversion that makes it meaningful: normally tools are dumb and the agent carries the protocol; here the engine is the agent's guardrail —it cannot skip a mandatory question or invent a branch, because the protocol lives in the graph and
deciderejects anything else—. - LLM authoring loop (spec 005): an LLM that writes the flows, with the validation ladder as the feedback loop:
- generate
- check()
- probe()
- regenerate
- goldens
The human reviews behavior, not wiring. And the anti-snake-oil comes by design: coverage is measured by the engine, so an LLM cannot fake it.
- Repeatable subflows (v1.1): the event format has reserved its slot since v1.
What remains to be proven
Horizontality is a hypothesis, not a result. The same core should work for retail, logistics, governance, or clinical protocols; for now, what exists are heavily verified fixtures. I also do not yet know how authoring will be packaged —CLI, MCP tools, both?— or where a human coverage waiver will be recorded. These are open questions in the spec, and I would rather leave them open than close them with hand-waving.
It is the same way of working I apply in agentic development consulting: the specification rules, the agent implements against it, and the tests seal the deal. The difference is that this experiment is mine and will grow in public. When the MCP adapter lands and the authoring loop starts writing real flows, I will write about it here.
More from the lab

Naiz Fit: configurable testing campaigns

Agentic development consulting

A simulator for Bizkaia's foral income tax

Coder: remote development environments with devcontainers for the whole team

Postiz: scheduling social media posts from your own server
