Content
# Reflow
A reactive, node-based visual programming language and editor — with an AI layer.
One live reactive graph, three synchronized surfaces: a **notebook** of expression
cells, a **canvas** of wired nodes, and **MCP channels** so Claude Code can read,
edit, and subscribe to the same running program. Nodes whose value is a UI spec
render **real React UI in place** — including a chat assistant that is itself
written in Reflow cells and can create new nodes.
| Notebook (cells + inline UI) | Canvas (nodes, chat rendering in-node) |
| --- | --- |
|  |  |
## Highlights
- **Reactive engine** (`@reflow/core`): dirty-propagation scheduler over a DAG with
memoized outputs and equality-pruned change events; recompute is **O(affected
subgraph)**. Measured: a 10,000-node chain updates in ~17 ms (**≈560k node
updates/sec**).
- **Turing complete**: recursion via lambda cells and function subgraphs (lazy `if` +
`call`), bounded `while` iteration. Proven by programs run as tests: a Turing
machine simulator and Rule 110, both verified against independent JS references.
- **Automatically persistent or ephemeral variables**: `count = var(0)` is
session-scoped; `best = pvar(0)` auto-loads on startup and auto-saves on write.
Persistence is a property of the variable — writes look identical.
- **One graph, every surface**: a notebook cell *is* a graph node (type `expr`),
dependencies auto-wired from free identifiers. Edit in notebook, canvas, chat, or
MCP — everything updates live everywhere.
- **UI in nodes**: any cell/node whose value is built with `ui.*` renders as real
React components inline (toggle `{}`/`ui` to see the raw spec). Event handlers are
serializable actions (`set('count', count + 1)`) dispatched back into the engine.
- **LLM as a language primitive**: `llm(prompt, {system, model}?)` is an async
reactive source — `null` while pending, then the text flows through the graph;
results cache per node+prompt, and prompt changes re-request reactively.
`agent(conversation)` additionally lets Claude **edit the program** (create/
update/remove cells) via a tool. Both run server-side on the
[Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk).
- **The chat is dogfooded**: the assistant UI is ~6 Reflow cells — `chatMessages =
pvar([])` (persistent transcript), `chatReply = … agent(chatConvo) …`, and a
`ui.*` view. Ask it to "create a cell that…" and the node appears, live.
- **MCP channels for Claude Code**: an MCP server exposes the running program —
`list_nodes / read_node / add_cell / update_cell / remove_cell / set_variable`,
plus **`subscribe(channel)`** (a channel = a node output) and long-polling
**`get_updates`** so an agent can react to data flowing through the graph.
## The language
Cells are single pure expressions (`name = expression`), JS-flavoured:
```text
count = var(0) // ephemeral variable
best = pvar(0) // persistent (auto-saved/restored)
doubled = count * 2 // recomputes when count changes
fib = n => n < 2 ? n : fib(n-1) + fib(n-2)
haiku = llm('write a haiku about ' + topic) ?? '…thinking'
view = ui.column(ui.h1('Counter'),
ui.text('Count: ' + count),
ui.button('+1', set('count', count + 1)))
```
Lambdas, ternary, structural `==`, arrays/objects, and a pure stdlib (`map filter
reduce range len sum sort join split slice push assoc merge keys values get str num
while(init, cond, body)`). Mutual recursion works (lambda-body references are
late-bound). A Turing machine, from the test suite:
```text
final = while({tape: {}, pos: 0, state: 'A', steps: 0},
s => s.state != 'H', s => tmStep(s), 100000)
```
## Architecture
**Code and data live together** in one hub process; every client is a peer:
```
┌────────────────────────────────────┐
browser editor ──────┤ apps/server — the HUB │
(SSE + ops) │ · @reflow/core Engine (the graph) │
│ · vars.json + graph.json persist │
Claude Code ─────────┤ · llm()/agent() via Claude Agent │
(MCP stdio bridge) │ SDK, agent edits applied live │
│ · REST /api/state /api/op │
curl / scripts ──────┤ · SSE /api/events (state stream) │
└────────────────────────────────────┘
```
- `packages/core` — the language + engine (parser, evaluator, scheduler, variables,
node registry, function graphs, llm/agent providers as injected interfaces). No DOM,
no network.
- `apps/server` — hub (`src/hub.ts`), Claude SDK providers (`src/providers.ts`), MCP
server (`src/mcp.ts`), demo seed. The reactive graph stays acyclic; Turing-complete
recursion/iteration lives in the function layer with depth/iteration caps, so the
editor can never hang.
- `apps/web` — React + Vite + @xyflow/react thin client: state mirrors in over SSE,
edits go back as ops.
## Run it
```bash
pnpm install
pnpm dev # hub :5200 + editor :5199 (real Claude via Agent SDK auth)
REFLOW_LLM_MOCK=1 pnpm dev:hub # deterministic mock LLM/agent instead
pnpm test # 97 core tests
pnpm e2e # 12 Playwright end-to-end tests (mock hub, auto-started)
pnpm verify:mcp # MCP client end-to-end check (needs a running hub)
```
Hook Claude Code up to the running program:
```bash
claude mcp add reflow -- pnpm --dir <repo>/apps/server mcp
# then: subscribe to a channel, e.g. "subscribe to the count cell and tell me when it changes"
```
Models default to `claude-haiku-4-5` for `llm()` and `claude-opus-4-8` for the chat
agent (`REFLOW_LLM_MODEL` / `REFLOW_AGENT_MODEL`, or per-call `{model}`).
## Verification
- **Core (vitest, 97 tests)**: parser/evaluator semantics; incremental-recompute
proofs (compute-count assertions); cycle detection/recovery; error propagation with
provenance; persistence round-trips through real files; recursive + mutually
recursive programs; **Turing machine + Rule 110 vs independent JS references**;
llm()/agent() async semantics (dedupe, reactivity, error paths, graph edits);
performance bounds.
- **E2E (Playwright, 12 tests)**: live values + inline UI rendering; action dispatch
through the hub; UI/raw toggling; cell editing with error recovery; fib typed into
the notebook; async llm() cells; **chat conversing, persisting across engine
restarts, and creating nodes via the agent**; pvar-vs-var restart semantics; canvas
editing, port-drag edge creation, cross-surface sync. Zero uncaught page errors.
- **MCP (`verify:mcp`)**: real MCP client over stdio exercises every tool, including
subscribe → external change → get_updates long-poll delivery.
- **Real-Claude spot checks**: `llm('Reply with exactly: pong')` → `"pong"`; chat
asked for "a cell named triple that multiplies count by 3" → agent created
`triple = count * 3`, which recomputed to 21 when count was set to 7.