A spreadsheet where every cell is a live, addressable capability. The grid is the runtime.
Quilt Live ⚡ · Playground ▶ · Studio 🎨 · IDE 🛠 · Synoptic 🎬 · 3D View · Patterns 📚 · Showcase 🌟 · Docs 📚 · Compare ⚖ · Tutorial 🎓 · Manifesto → · Examples →
Both are production-grade. Same engine, same sheet format, different tradeoffs.
| Need | Use TypeScript | Use Rust |
|---|---|---|
| Browser simulator / web UI | ✅ | ✅ (axum) |
| TUI for the terminal | ✅ | ✅ (crossterm) |
| MCP server for Claude Code / Cursor / agents | ✅ | ✅ |
| Single static binary, no Node.js | ❌ | ✅ |
| Embedded / IoT / edge (RPi, ESP32, no_std) | ❌ | ✅ |
| Strict memory guarantees in a sandboxed cell | ❌ | ✅ (rhai) |
| High-throughput cell evaluation | ✅ (compiled) | |
| Embed in a web app or Node service | ✅ | ✅ (axum) |
| Static cross-compilation to any platform | ❌ | ✅ |
| WASM target for the browser | 🔜 (planned) | ✅ (planned) |
The two repos share the same sheet format (YAML) and the same conceptual model. A sheet that runs on one runs on the other (with a few language-specific quirks documented in
docs/harness-guide.md).
Quilt is a reactive, typed, cellular runtime. The spreadsheet is the control plane. The cell is the universal IO primitive.
- A cell can be a value, formula, api, program, sensor, listener, router, or io.
- A cell reference is a stable address, not a coordinate.
- A cell can route based on who called it (
caller.row > 10→ use Model A). - The whole sheet is an MCP server. Every named cell is an MCP tool.
- It's reactive by default. Change one cell, every dependent rewires.
- Per-context memoization: same cell called from different callers can return different cached values.
The paradigm shift, in one line: A cell is not a value. A cell is a live, typed, addressable capability. The spreadsheet is not a document. The spreadsheet is the runtime.
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ┌──────┐ ┌─────────┐ ┌──────┐ │
│ │ 📦 │ ─wire──▶│ ƒ │ ─wire──▶│ 🔔 │ │
│ │ $5K │ │ $2.5K │ │ ✓ │ │
│ └──────┘ └─────────┘ └──────┘ │
│ ▲ ▲ │
│ │ │ │
│ ┌──────┐ ┌──────┐ │
│ │ 📦 │ │ 📦 │ │
│ │$1.8K │ │ $600 │ │
│ └──────┘ └──────┘ │
│ rent food │
│ │
│ ▲ value cell ▲ formula cell ▲ listener cell │
│ │
└─────────────────────────────────────────────────────────────────┘
Edit a value. Formulas recompute. Listeners fire. All live.
This is a real Quilt engine running. The wires are real. The propagation is real. The whole thing is real.
→ Open Quilt Live in your browser (70 KB, no build, no install, works offline)
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 📦 │ │ ƒ │ │ ▶ │ │ 👁 │ │ 🌐 │
│ value │─▶│ formula │─▶│program │─▶│ sensor │─▶│ api │
│ 5,000 │ │ 2,520 │ │ async │ │ polled │ │ remote │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 🔔 │ │ ↪ │ │ 🔌 │
│listener │ │ router │ │ io │
│ fires │ │context │ │ device │
└─────────┘ └─────────┘ └─────────┘
| Kind | What it is | Use when |
|---|---|---|
| value | a static value | The data is known at sheet-author time, or changes externally |
| formula | a reactive expression | The value is computed from other cells. Sync, pure |
| program | a small async expression | The computation needs async, side effects, or runtime state |
| sensor | a polled input source | You're reading from a timer, GPIO, BLE, or external feed |
| api | an outbound call | You're calling out to an external service |
| listener | fires on changes | You want to alert, log, or write to disk on change |
| router | caller-context-aware dispatch | Multiple callers need different outputs based on context |
| io | an outbound port | You're driving a physical actuator (LED, relay, motor) |
Time ──────────────────────────────────────────────────────────▶
t=0 set budget.total = 5000 ─┐
│
t=1 set spend.rent = 1800 ─┤
│ cascade: 3 cells recompute
t=2 set spend.food = 600 ─┤ ─▶ spent = 2720
│ ─▶ remaining = 2280
t=3 set spend.transit = 120 ─┘ ─▶ percent = 0.544
─▶ status = "ok"
─▶ alert listener checks condition
You write the cells. The engine handles the propagation. The order of writes doesn't matter — the engine computes in dependency order, topologically.
┌──────────────────────────────────────────────────────────────┐
│ Quilt (your code) │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ Sheet (.yaml) │ │ Engine │ │ Harness │ │
│ │ │ │ │ │ │ │
│ │ cells: │ │ reactive DAG │ │ MCP server │ │
│ │ - id: x │─▶│ per-context │─▶│ CLI │ │
│ │ kind: value │ │ memoization │ │ web UI │ │
│ │ - id: y │ │ topological │ │ library API │ │
│ │ kind: for..│ │ evaluation │ │ │ │
│ │ │ │ │ │ │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │ │ │ │
└────────────┼───────────────────┼──────────────────┼──────────┘
▼ ▼ ▼
┌────────┐ ┌─────────┐ ┌──────────┐
│ 8 cell│ │ formulas │ │ external │
│ kinds │ │ programs │ │ systems │
│ │ │ listeners│ │ │
└────────┘ └─────────┘ └──────────┘
Three layers, cleanly separated:
- Sheet (declarative YAML) — what cells exist, how they relate
- Engine (the runtime) — the reactive DAG, evaluation, propagation
- Harness (the surface) — MCP, CLI, web, library — your entry point
You can use any layer in isolation. The sheet format is the public contract.
You already know spreadsheets — columns of numbers, formulas that snap back to life when you change an input. Now keep the spreadsheet, but make every cell a living thing. A cell isn't just a number in a box. It can be a sensor reading, an API call, a program, a model, a light switch, a listener, a policy. Give every cell a stable name, let cells point at each other, and when one cell changes, everything downstream rewires itself — automatically, instantly, visibly.
That's Quilt. A sheet is a YAML file, and that file is the whole system: the data, the logic, the I/O, the routing, the alerting. No build step, no services to deploy, no glue code to maintain. Run the sheet and cells compute. Expose the sheet and every named cell becomes a tool an agent can call. Edit the sheet and the running system changes. The grid isn't a picture of the system. The grid is the system.
The mental leap, in two moves:
- A cell is not a value. A cell is a contract. It's a stable
address you can plug anything into.
compass.headingdoesn't hold "a number"; it holds whatever the compass says right now. Swap the implementation — real NMEA, a simulator, a test stub — and every cell that reads it keeps working. That's dependency injection, done by editing YAML. - A spreadsheet is not a document. A spreadsheet is a runtime.
When you change
target_headingfrom270to090, you aren't editing a record of the boat; you're commanding it. Formulas recompute, listeners fire, actuators move. The file isn't the log of the system. The file is the nervous system.
Every real system is a pile of event handlers, webhooks, cron jobs, database rows, and config spread across half a dozen repos, stitched together with glue code that no single person can hold in their head. Quilt collapses that pile into one grid. Each integration point is one named cell. Connections are references, not code. Changes propagate by themselves. And the whole thing stays auditable: the dependency graph is the architecture diagram, it's always current, and it's in a format your whole team can read and diff. When something goes wrong you don't grep five services — you open the sheet, find the red cell, and see exactly which inputs changed.
For the agent era it matters differently. Every sheet is an MCP server, so Claude Code, Cursor, or any MCP client reads cells as resources and calls them as tools. Humans and agents share one grid: the agent writes a cell, you see the write; you override a cell, the agent sees the override on its next read. Caller-aware routing means one sheet serves many tenants and tiers without forking. And the jazz → classical flywheel — start with an LLM cell, watch the calls, distill the common cases into a formula, watch the cost drop — becomes a visible, editable process instead of a refactor buried in a codebase. If you're wiring sensors, models, APIs, and agents into one system, and you want it reactive, auditable, and agent-readable: this is the substrate.
┌──────────────────────────────────────────────────────────┐
│ Quilt (TS) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ parse │───►│ engine │◄──►│ cells │ │
│ │ (YAML) │ │ (graph) │ │ (8 ks) │ │
│ └──────────┘ └────┬─────┘ └──────────┘ │
│ │ │
│ ┌───────────┼────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ CLI │ │ MCP │ │ TUI │ │
│ │ (node) │ │ (mcp) │ │ (ansi) │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
│ ┌────────┐ │
│ │ Web │ ← /landing/quilt-live.html + simulator│
│ │ UI │ ← browser-native, no build step │
│ └────────┘ │
└──────────────────────────────────────────────────────────┘
The Rust port has the same shape — same 8 cell kinds, same engine, same CLI/MCP surfaces. The difference: node + JavaScript instead of tokio + rhai. See superinstance/quilt-rust.
A spreadsheet where every cell is a live API endpoint, and changing one cell rewires every dependent sensor, model, and agent.
Five ideas, in order. Read them once and you'll think in cells.
A sheet is YAML: an id, optional axes (what rows and columns
mean), and a cells list. Each cell is one entry. No build step, no
schema to register, no framework — a sheet is data that the engine
brings to life.
id: hello
cells:
- id: greeting
kind: value
value: "Hello, Quilt!"A cell is named, not numbered. A1 tells you nothing; compass.heading
tells you everything. Ids are dot-namespaced (fleet.boat1.rudder),
stable across reordering, and the thing other cells reference. A
formula names its inputs explicitly — rename a cell and its dependents
break loudly, which is the point: dependencies are visible, not
implicit.
cells:
- id: hello
kind: value
value: "Hello"
- id: greeting
kind: formula
expr: "=hello + ', world!'"Pure cells — value and formula — recompute on demand, the same
lazy way Excel does: a change marks dependents stale, and the next
read of a dependent recomputes it. Effectful cells — api, program,
router — run when called and cache their result per caller context.
Listeners are the eager ones: the moment a watched cell changes, they
fire. Same spreadsheet, extended to async and side effects.
- id: temp
kind: value
value: 21
- id: feels_like
kind: formula
expr: "=temp * 9 / 5 + 32"Every call carries context — row, column, identity, trace. A
router cell evaluates rules against that context and delegates. The
same address can return a different value to a premium caller than to
a free one, to row 5 than to row 50. This is the primitive nobody else
has: position is policy.
- id: pick
kind: router
rules:
- when: 'caller.row > 10'
route: { cell: fast }
- when: 'true'
route: { cell: precise }A sensor streams values in — an adapter pushes readings. An io
cell is a port: a webhook, a GPIO pin, an actuator — it can receive
and send. The address doesn't care which side you're on: read it and
you get the latest state; write it and the grid reacts.
- id: rudder
kind: io
direction: out
port: actuator:rudderAny cell can reference any other cell. That's the whole composition
story — you don't import modules, you point at addresses. A formula
that reads sensor.temp is bound to that sensor; a listener that
watches is_dark is bound to that decision; a router that delegates
to models.precise is bound to that model. Change the target cell and
every binding follows. This is the five-layer abstraction — and it's
why the grid composes the way a real system should: by name, not by
nesting.
No install. No clone. Just open it.
| What | Try it | Best for |
|---|---|---|
| Quilt Live | → Open the page | Full reactive data OS in your browser. Click-to-try grid, save state as a cookie or downloadable .html. One file, the whole app. |
| Live simulator | → Open the page | Side-by-side code editor + runtime view + dependency graph. Edit a YAML sheet, see the cells update in real time. |
Both run entirely in the browser. No build, no install, no server.
Want a local copy? You can also download
quilt-live.htmlas a single file and run it offline. The button is right there in the app's top bar.
A portable, reactive data OS in one HTML file. ~70 KB, zero dependencies. Every cell in the grid is a live, addressable capability. Open the link above, change a value, watch the formulas recompute. Save the state as a cookie, or download the whole app with your state baked in. The downloaded file IS the app — open it on any device, even offline, and your work is right there.
→ Open Quilt Live · Source on GitHub · See the 54 examples
Read the 5-year roadmap for the bigger picture — Quilt on ESP32, mesh networking, agents as sheets, the personal data mesh. We have sketches for quilt-esp32 (microcontroller runtime), quilt-mesh (peer-to-peer sync), and quilt-agent (LLM agents as sheets).
# 1. Clone
git clone https://github.com/superinstance/quilt.git
cd quilt
# 2. Install (npm workspaces)
npm install
# 3. Run a sheet
npx @quilt/cli run examples/boat-autopilot/sheet.yaml
# 4. Serve as MCP (then point an MCP client at it)
npx @quilt/cli serve examples/boat-autopilot/sheet.yaml
# 5. Or use the TypeScript API
node -e '
import("@quilt/core").then(async ({ QuiltEngine, parseSheet }) => {
const engine = new QuiltEngine({ id: "demo" });
engine.loadSheet(parseSheet("id: demo\ncells:\n - id: hello\n kind: value\n value: hello, world"));
const v = await engine.get("hello", {});
console.log(v.data);
});
'

A sheet can hold many different kinds of cells at once — each one a
different kind of capability, all addressable from the grid.
| Kind | What it is | Evaluator | Example |
|---|---|---|---|
value |
Static data. No dependencies. | direct | kind: value, value: 42 |
formula |
Reactive expression. Re-evaluates on change. | new Function |
kind: formula, expr: =a + b |
api |
HTTP endpoint. Fetched on call. | fetch (async) |
kind: api, endpoint: https://... |
program |
Inline JavaScript async function. | AsyncFunction |
kind: program, code: | ... |
sensor |
Push-only value. Adapter writes, formula reads. | external adapter | kind: sensor, source: mqtt://... |
io |
Bidirectional port. | external adapter | kind: io, port: gpio17, direction: out |
listener |
Triggers on watched cell change. | engine propagation | kind: listener, watch: [x] |
router |
Caller-aware policy. Delegates to a target cell. | eval_when |
kind: router, rules: [...] |
From examples/boat-autopilot/sheet.yaml:
id: boat-autopilot
version: "1"
cells:
- id: heading
kind: sensor
source: "nmea:/dev/ttyUSB0"
description: The boat's current heading in degrees.
- id: target_heading
kind: value
value: 270
description: Where the autopilot should steer.
- id: error
kind: formula
expr: =((target_heading - heading + 540) % 360) - 180
description: The signed angular error, in [-180, 180].
- id: rudder
kind: formula
expr: =clamp(error * 0.5, -30, 30)
description: The commanded rudder angle.
- id: rudder_cmd
kind: io
port: rudder_actuator
direction: out
description: The actual rudder command sent to the actuator.What this gives you:
┌──────────────────┬─────────┬─────────┐
│ ID │ KIND │ VALUE │
├──────────────────┼─────────┼─────────┤
│ heading │ sensor │ 265 │
│ target_heading │ value │ 270 │
│ error │ formula │ 5 │
│ rudder │ formula │ 2.5 │
│ rudder_cmd │ io │ 2.5 │
└──────────────────┴─────────┴─────────┘
Change target_heading to 090 and watch the chain recompute. The boat's actual rudder is commanded to turn the boat toward 090. That's it. That's the whole autopilot.
Let's build something real from nothing: a light that turns on when a sensor reading crosses a threshold. A twilight lamp for a room — the kind of thing you'd put on a Raspberry Pi, except the whole controller is a YAML file.
The commands below use
quilt, the repo's CLI binary (available afternpm install && npm run build— see Quick start).
quilt init room-light
# ✓ Scaffolded room-light.quilt.yamlThis creates room-light.quilt.yaml with a starter sheet: a value,
a formula, and a pure computation. We're replacing the cells with our
own.
Open room-light.quilt.yaml and give it these five cells:
id: room-light
title: "Twilight Light"
description: "A light that turns on when the room gets dark"
version: 0.1.0
cells:
- id: ambient.light
kind: sensor
source: simulated
rate: 1000
default: 400
unit: lux
description: "Current ambient light level"
- id: threshold
kind: value
value: 200
unit: lux
description: "Below this, the room counts as dark"
- id: is_dark
kind: formula
expr: "=ambient.light < threshold"
description: "True when it's dark enough to need the light"
- id: light.state
kind: formula
expr: "=is_dark ? 'ON' : 'OFF'"
description: "The light's command state, derived from darkness"
- id: light.actuator
kind: io
direction: out
port: gpio:relay1
description: "The physical relay the light is wired to"Walk through it, cell by cell:
ambient.light— asensor: input from the outside world.source: simulatedmeans no real hardware;default: 400gives it a starting reading so the sheet works out of the box. In production the source would be something likei2c:/dev/i2c-1ormqtt://broker/topic, and an adapter would push readings into the cell.threshold— avalue: a configuration knob. Change this one number and the light's behavior changes, with zero logic edits.is_dark— aformula: the decision.=ambient.light < thresholdreferences two cells by id. The engine auto-detects the dependency edges by scanning the expression — you never declare them.light.state— aformulathat turns the boolean into a command string. Note the ternary: formulas are JavaScript expressions, withabs,min,max, andclampavailable.light.actuator— aniocell,direction: out: the port the relay is wired to. In a real deployment a harness binds this port to a GPIO pin. Here it's the visible output slot of the system.
quilt inspect room-light.quilt.yaml──────────────────────────────────────────────────────────────────────
Sheet: room-light
Cells: 5
──────────────────────────────────────────────────────────────────────
[sensor] (1)
ambient.light — Current ambient light level
[value] (1)
threshold — Below this, the room counts as dark
[formula] (2)
is_dark ← ambient.light, threshold — True when it's dark enough to need the light
light.state ← is_dark — The light's command state, derived from darkness
[io] (1)
light.actuator — The physical relay the light is wired to
The ← arrows are the dependency edges the engine found. You can see
the whole system at a glance — this is the architecture diagram.
quilt run room-light.quilt.yaml▶ Running sheet: room-light (Twilight Light)
5 cells loaded.
◉ ambient.light [sensor ] ✓ 400
○ threshold [value ] ✓ 200
ƒ is_dark [formula ] ✓ false
ƒ light.state [formula ] ✓ "OFF"
⇆ light.actuator [io ] … ∅
400 lux of daylight, threshold 200: not dark, light OFF. The glyphs
are the cell kinds — ◉ sensor, ○ value, ƒ formula, ⇆ io.
Two honest ways to see the reactivity, depending on whether you want to edit or to watch.
The edit way — the sheet is the source of truth. Change
default: 400 to default: 120, save, and run again:
◉ ambient.light [sensor ] ✓ 120
ƒ is_dark [formula ] ✓ true
ƒ light.state [formula ] ✓ "ON"
The room got dark, so the light turned on. You changed one number in a file and the system recomputed.
The live way — a running engine reacts to pushes. Ten lines of Node, using the same library the CLI uses:
import { readFile } from 'node:fs/promises';
import { QuiltEngine, parseSheet } from '@quilt/core';
const sheet = parseSheet(await readFile('room-light.quilt.yaml', 'utf8'));
const engine = new QuiltEngine(sheet.id);
engine.loadSheet(sheet);
console.log('daylight:', (await engine.get('light.state')).data); // OFF
await engine.push('ambient.light', 120); // the room goes dark
console.log('is_dark:', (await engine.get('is_dark')).data); // true
console.log('light.state:', (await engine.get('light.state')).data);// ON
await engine.push('ambient.light', 500); // morning
console.log('light.state:', (await engine.get('light.state')).data);// OFFdaylight: OFF
is_dark: true
light.state: ON
light.state: OFF
engine.push feeds the sensor — that's exactly what a real adapter
does (MQTT subscription, GPIO interrupt, webhook). The formula chain
recomputes on the next read. No events, no callbacks, no wiring — just
cells.
Why not
quilt set?quilt set room-light.quilt.yaml ambient.light 100works, but each CLI invocation is its own session: it loads the YAML, sets the value in memory, prints✓ set ambient.light = 100, and exits. The YAML file is unchanged, and the next command starts from the file again.setis for scripting and agents; persistence is the file, liveness is your harness, the TUI, the simulator, or aservesession.
A formula that flips a string is nice; a system that acts is better. Add a listener and a program cell:
- id: log.switch
kind: listener
watch: [is_dark]
action: log.event
description: "Fires whenever darkness changes"
- id: log.event
kind: program
code: |
const dark = (await runtime.get('is_dark')).data;
return { event: dark ? 'lights-on' : 'lights-off', at: Date.now() };The listener is eager: the instant is_dark changes, it calls
log.event, which runs arbitrary async JavaScript — log to a file,
POST a webhook, flash an LED. The program cell gets three handles:
input, caller (the context), and runtime — with
runtime.get / runtime.set / runtime.call it can read, write, and
invoke any other cell. That's the "anything" escape hatch; use it
sparingly and prefer formulas for pure math.
quilt serve room-light.quilt.yaml --mcp
# [quilt] serving sheet 'room-light' as MCP server on stdioPoint Claude Code or Cursor at it (config in the next section) and the
agent can read cell__is_dark, ask what's in the sheet, and call
cell__log_event to trigger the program. Your light just got an API —
and the API is the whole grid.
quilt is deliberately small — the power lives in the runtime and the
embedding surfaces. This is the entire command surface:
| Command | What it does |
|---|---|
quilt init [name] |
Scaffold <name>.quilt.yaml (default my-quilt-sheet) with a starter sheet |
quilt run <sheet> |
Load the sheet, evaluate every cell once, print id / kind / status / value |
quilt serve <sheet> |
One-shot serve: load and print the sheet's current state |
quilt serve <sheet> --mcp |
Serve the sheet as an MCP server on stdio — every cell becomes a tool |
quilt get <sheet> <cell> |
Print a cell's CellValue as JSON |
quilt set <sheet> <cell> <value> |
Set a cell's value in a fresh session and print confirmation (does not persist) |
quilt test <sheet> |
Evaluate every cell; print ✓/✗ per cell; exit 1 if anything failed |
quilt inspect <sheet> |
Show the sheet grouped by kind, with dependency arrows and descriptions |
quilt help / -h / --help |
Print usage |
Details worth knowing:
setvalue parsing — the value is parsed as JSON first, and falls back to a plain string:quilt set s.yaml x 42sets the number42,quilt set s.yaml x '"42"'sets the string"42",quilt set s.yaml x '{"a":1}'sets an object. Bare words become strings.runglyphs — the kind symbols:○value,ƒformula,↗api,⚙program,◉sensor,⚡listener,⇄router,⇆io. Status marks:✓ready,✗error,…idle/pending,∅no value.- Stateless by design — every invocation loads the YAML fresh.
run,inspect, andtestare read-only;setmutates a throwaway session. Long-lived state belongs to a harness, the TUI, the simulator, or an MCPservesession. - Exit codes — usage errors exit
1;testexits1if any cell failed; everything else exits0.
Add to your MCP config (~/.config/claude-code/mcp.json or similar):
{
"mcpServers": {
"quilt": {
"command": "npx",
"args": ["@quilt/cli", "serve", "/path/to/your/sheet.yaml", "--mcp"]
}
}
}The --mcp flag is what puts the sheet on stdio. Without it, serve
prints a one-shot view and exits — an MCP client would see nothing.
Then in your conversation:
"What cells are in this sheet?"
"Read the
ruddercell.""Set
target_headingto 045.""What cells depend on
heading?"
Claude calls the MCP tools directly. You see the cell values; you don't write code.
The mapping is direct — there is no translation layer:
- Every named cell → one MCP tool, named
cell__<id>with dots sanitized to underscores (compass.heading→cell__compass_heading). The tool acceptsinput(passed to the cell), plusrowandcolumn(caller context for routing). - The whole sheet → an MCP resource at
quilt://<sheet>/sheet, plus one resource per cell atquilt://<sheet>/cell/<id>.
In a repo clone, substitute
npx @quilt/cliwith the builtquiltbinary (the npm packages aren't published yet) — e.g."command": "/path/to/quilt/node_modules/.bin/quilt".
Claude Code — either add it with the CLI:
claude mcp add quilt -- npx @quilt/cli serve /path/to/your/sheet.yaml --mcpor by editing ~/.config/claude-code/mcp.json (the config in the
section above — don't forget --mcp).
Cursor — project-level config in .cursor/mcp.json (or
~/.cursor/mcp.json for all projects):
{
"mcpServers": {
"quilt": {
"command": "npx",
"args": ["@quilt/cli", "serve", "/path/to/your/sheet.yaml", "--mcp"]
}
}
}A worked flow — with the boat sheet served, an agent can:
"What cells are in this sheet?" — reads the
quilt://boat-autopilot/sheetresource."Call
cell__heading_errorfor boat-2." — invokes the formula withrow: "boat-2"."What's the current rudder command?" — calls
cell__rudder_command.
One honest caveat: the current toolset is call-based. Calling a
value or formula cell returns its current value; calling an
effectful cell invokes it with your input. There's no set/push tool
yet, so agents can read and invoke, but writes flow through your
harness — or through a program cell that calls runtime.set.
@quilt/core is a plain ESM library — no native deps, TypeScript,
~1,500 commented lines. It's the same engine the CLI, MCP, and TUI
all sit on. Embed it in a Node service, a web server, an agent
runtime, or a test suite.
npm install @quilt/coreThe engine API, in one breath:
| Method | Signature | Notes |
|---|---|---|
loadSheet |
(sheet: SheetDef) => void |
Loads a parsed sheet; resets engine state and rebuilds the graph |
defineCell |
(def: CellDef) => Cell |
Add one cell; throws on duplicate id |
register |
(def: CellDef) => Cell |
Dynamic registration (agents defining cells at runtime); adds declared deps |
get |
(id, ctx?) => Promise<CellValue> |
Pull a value, computing if needed; never throws — errors come back as status: 'error' |
set |
(id, value, ctx?) => Promise<void> |
Write a value and propagate; throws on unknown cell |
call |
(id, input?, ctx?) => Promise<CellValue> |
Invoke a cell as a capability; behaves as get for pure cells |
push |
(id, data, ctx?) => Promise<void> |
Feed a sensor/io cell from an adapter; throws for other kinds |
subscribe |
(id, cb(value, prev), filter?) => string |
Watch one cell; returns a subscription id |
unsubscribe |
(subId) => void |
Stop watching |
getCell |
(id) => Cell | undefined |
Raw cell — read .dependencies and .dependents |
listCells |
(kind?) => Cell[] |
All cells, optionally filtered by kind |
getTraces |
(limit = 100) => EvaluationTrace[] |
Recent evaluations (debugging, cost accounting) |
exportDefs |
() => CellDef[] |
Serialize runtime state back to definitions |
Plus the parser and context helpers: parseSheet(yaml),
validateSheet(raw), serializeSheet(sheet), emptyContext(),
extendContext(parent, childId, extra?), contextKey(ctx), and
evalWhen(when, ctx).
The boat demo (examples/boat-autopilot/demo.ts) is built from exactly
this pattern — load, push, read, per-boat context:
import { readFile } from 'node:fs/promises';
import { QuiltEngine, parseSheet } from '@quilt/core';
const sheet = parseSheet(await readFile('examples/boat-autopilot/sheet.yaml', 'utf8'));
const engine = new QuiltEngine(sheet.id);
engine.loadSheet(sheet);
// Two boats, one sheet, caller-aware rows:
for (const boat of [{ id: 'boat-1', heading: 180 }, { id: 'boat-2', heading: 175 }]) {
await engine.push('compass.heading', boat.heading);
const err = await engine.get('heading.error', { row: boat.id });
console.log(boat.id, err.data); // the signed error for that boat
}
// Watch a cell forever:
const sub = engine.subscribe('compass.heading', (v, prev) => {
console.log('heading:', prev.data, '→', v.data);
});Caller context is just an object — pass row, column,
identity, or metadata to any call, and every cell downstream sees
it:
const ctx = {
row: 'boat-1',
identity: { id: 'user-1', type: 'human', tags: ['premium'] },
};
const v = await engine.call('model.router', input, ctx);@quilt/tui is a keyboard-driven terminal view of a running engine —
live cell grid, dependencies and dependents panels for the selected
cell, no full-screen takeover, plays well with tmux.
npx quilt-tui room-light.quilt.yaml| Key | Action |
|---|---|
j / k / arrows |
Move selection |
g / G |
Jump to top / bottom |
s |
Set the selected cell to a new value (JSON or bare string) |
e |
Edit (read-only in MVP — use s) |
: |
Command mode — reload, help, quit |
r |
Reload cell values from the engine |
q / Ctrl-C |
Quit |
A worked flow: open the boat sheet, k down to desired.heading,
press s, type 090, Enter — the engine sets the cell and
propagates; press r to refresh the grid and watch heading.error
flip sign. In s mode, 42 is a number, "42" is a string, and
{"a":1} is an object.
landing/simulator.html is a self-contained
simulator — open it in any browser, even offline. Left pane: the sheet
source. Right pane: the live runtime. Below: the dependency graph.
Pick a preset (Boat Autopilot, Model Router, Anomaly Detector, Agent
Dashboard), edit the YAML, hit Apply, and watch cells recompute in
real time. For the full single-file experience, try
Quilt Live
(landing/quilt-live.html in this repo) — the same reactive grid as a
portable, save-your-state app. landing/index.html
is the landing page with the animated grid demo;
landing/tutorial.html is the interactive
tutorial. None of them need a build step.
Quilt is built on 5 layers of "addressing as composition." Understanding them is the difference between using Quilt and thinking in Quilt.
Layer 0 ADDRESS A stable id, not a coordinate. Not a URI. A name.
Layer 1 SPATIAL row / column carry context. Position is policy.
Layer 2 REACTIVE when an address changes, dependents re-evaluate.
Layer 3 BIDIRECTIONAL same address is readable AND writable.
Layer 4 COMPOSING writing an address IS binding to it.
The Rust port honors the same five layers. The data model is identical. Only the runtime differs.
@quilt/core— the reactive cell engine. TypeScript, ESM, no native deps. ~1,500 lines, heavily commented.@quilt/cli—init / run / serve --mcp / get / set / inspect / test. The entry point.@quilt/mcp— exposes every cell as an MCP tool, every sheet as an MCP resource.@quilt/tui— terminal-native view of a running engine. Live cell grid, dependencies panel, key bindings. Plays well with tmux.
The original four:
- boat-autopilot — sensors, PID, voice intent, model router. The killer demo.
- agent-dashboard — tasks, status, shared human+agent workspace.
- model-router — caller-aware model selection.
- sensor-anomaly — self-tuning anomaly detector.
Six new production-grade examples:
- weather-monitor — three sensors → heat-index formula → listener alerts → caller-aware router.
- chat-router — LLM routing by tier (premium/standard/free) and message length.
- ab-test-router — deterministic A/B test using FNV-1a hash + bucket router.
- iot-dashboard — three thermometers → room status → building status, with alerts.
- rate-limiter — token-bucket rate limiter with per-caller state.
- task-scheduler — reactive task scheduler with overdue listener.
- predictive-maintenance — per-machine rows, sensor → model → alert
- npc-behavior — game NPC behavior
- edge-anomaly-detection — Raspberry Pi / industrial gateway
- landing/index.html — landing page with animated grid demo
- landing/quilt-live.html — the Quilt Live single-file reactive data OS (live page)
- landing/simulator.html — live side-by-side simulator (code + runtime + dependency graph) (live page)
- All work offline. Just open them in a browser.
- Manifesto — the 10-point declaration
- Architecture — engine internals
- Tutorial — 5 chapters, about an hour
- Recipes — 10 common patterns, copy-paste
- Comparison — how Quilt differs from n8n, LangGraph, etc.
- Security — the trust model, what to do in production
┌────────────┐
│ CellDef │ ← a YAML object
│ id, kind, │
│ kind- │
│ specific │
│ fields │
└─────┬──────┘
│ load_sheet
▼
┌────────────┐
│ Cell │ ← a runtime object
│ def, value,│
│ deps, │
│ dependents │
└─────┬──────┘
│ set / push / call
▼
┌────────────────────┐
│ propagation │
│ - mark stale │
│ - fire listeners │
│ - recompute │
│ - notify subs │
└─────────┬──────────┘
│
▼
┌────────────────────┐
│ CellValue { │
│ data, status, │
│ error, │
│ effects, │
│ computed_at │
│ } │
└────────────────────┘
Every get reads. Every set writes. Every call invokes. Every push is an inbound event. Every subscribe is a notification stream. The cell kind determines the how; the engine handles the when.
Quilt is intentionally not a tree of nodes. It's a flat Map<CellId, Cell> with explicit dependencies and dependents sets. Why?
- Cycles are allowed (a listener can watch the cell that triggers it; the engine just fires it again on the next change).
- Order doesn't matter — you can add a cell at any time.
- Debugging is trivial: every dependency is named, every dependent is named, you can
console.logthe cell.
Trees are great for hierarchies (DOM, AST, file system). Quilt is not a hierarchy.
A formula is lazy: it recomputes the next time someone calls get on it. This is the right thing for a spreadsheet — millions of cells can exist, but only the ones the user looks at need to be evaluated.
A listener is eager: it fires the moment the watched cell changes. This is the right thing for an event handler — by definition, it needs to react.
A CellValue is { data, status, error, effects, computed_at }. Why the wrapper?
statuslets the UI render stale/error/loading states without inspecting the data.errorcarries the error message separately from the data, so a UI can show both.effectsrecords what the cell did (network call, storage read, etc.) for tracing, cost accounting, and decomposition.computed_atlets you show freshness: "this value is 3 seconds old."
This is what makes Quilt a runtime, not a data structure.
Historical reasons: the engine was prototyped in JavaScript before Rust was a serious option. new Function gives you the full JavaScript language, including async/await, which is what program cells use.
The trade-off: new Function is not sandboxed by default. A malicious program cell can read the filesystem, make network calls, etc. The mitigation is documented in docs/security.md. The Rust port uses rhai which IS sandboxed by default — see superinstance/quilt-rust.
The TypeScript version is gaining a WASM-cell-sandbox as a v0.2 feature to close the gap.
Formulas are lazy. When an input changes, the engine marks the
dependent stale but doesn't compute it until someone reads it —
exactly like Excel. If you see "status": "stale", that's the engine
telling you "this needs a recompute," and the very next get performs
it. If the dependent is an api/program/router cell, it's
different: effectful cells are never auto-recomputed by upstream
changes — they run when called. Call it (with a fresh context) and it
will evaluate.
That's per-context memoization, working as designed. Effectful cells
cache their result keyed by the caller context (row, column,
identity.id, identity.tags). Same context → same cached value,
even if upstream cells changed. Force a fresh evaluation by changing
the context (a different row, or an identity tag) or by setting
the cell itself. This is also why the same cell can serve many callers
at once — each gets its own cached answer.
push/set notify subscribers before propagation marks dependents
stale, so inside a callback the downstream cells may still hold their
previous values. After await engine.push(...) returns, propagation
is complete and everything is fresh. If you must read dependents
inside a callback, wait a tick
(await new Promise(r => setImmediate(r))).
Yes — the graph is a flat Map<CellId, Cell> with explicit dependency
sets, not a tree, and feedback loops are allowed: a listener can watch
the very cell it triggers and will simply fire again on the next
change. Keep pure chains (formulas) acyclic — a formula cycle has
no convergent answer. Effectful feedback (sensor → formula → listener
→ actuator → sensor) is the intended pattern.
Not by default — and this is the one to read carefully.
docs/security.md is the full trust model. The
short version: formulas compile via new Function, program cells via
new AsyncFunction; they run in the host process with the full
JavaScript language and are not a security boundary. Trust the
author of the sheet — the author defines what cells call, what they
connect to, what code they run. Treat anything calling into the
sheet as untrusted input. For production: run the engine in a
container or worker, use worker_threads or isolated-vm, whitelist
endpoints, keep secrets in env vars. The Rust port uses rhai, which
is sandboxed by default; a WASM sandbox for the TS port is planned
for v0.2.
api and program cells are asynchronous. The engine de-dupes
concurrent calls to the same effectful cell: while one evaluation is
in flight, a second call awaits the same promise (the inflight map
in engine.ts). A cell mid-evaluation reports status: 'computing'.
Results are cached per context after completion.
Three answers, all real:
quilt inspect sheet.yaml— shows every cell's dependencies asid ← a, b, c.- The engine API:
engine.getCell('heading').dependents— the reverse index, exactly the cells that would rewire ifheadingchanged. - The TUI: select a cell and read its dependencies/dependents panels.
Two separate things, both common:
- Sensors are push-only.
geton a sensor returns the last pushed value (ordefault, if set) — nothing polls, ever. If no adapter has pushed and there's nodefault, you get∅. Feed it withengine.push(quilt setworks as a manual stand-in in a pinch). - The CLI is stateless. Each
quiltcommand loads the YAML fresh;setmutates a throwaway session and exits. The file is the source of truth. Long-lived reactivity lives in a harness, the TUI, the simulator, orquilt serve --mcp.
| Kind | What it is | When to use it |
|---|---|---|
value |
Static data. No dependencies. | Constants, config, state you change rarely. The leaves of the graph. |
formula |
Pure reactive expression, lazily recomputed on change. | Anything derivable from other cells. Prefer this for all math. |
api |
HTTP endpoint (or model: / mcp:// pseudo-endpoint), fetched on call. |
External data or model calls that should run when invoked, not on a schedule. |
program |
Inline async JavaScript. | Logic beyond expressions: orchestration, branching, runtime.get / set / call chains. Use sparingly. |
sensor |
Push-only inbound stream; adapter writes, formulas read. | Anything that arrives on its own timing — MQTT, GPIO, serial, webhooks. |
io |
Bidirectional port: receives and sends. | Webhooks, actuators, hardware pins — the address that faces the world. |
listener |
Eager trigger that fires when a watched cell changes. | "If X happens, do Y" — alerts, escalations, actions. |
router |
Caller-aware policy; delegates to a target based on context. | Serving many tenants / tiers / models behind one address. |
- Address — a cell's stable, dot-namespaced id
(
fleet.boat1.rudder). Not a coordinate. Survives reordering and refactors. - Caller — whoever triggered an evaluation: a human, an agent, a sensor, another cell. The caller's context travels with the call.
- Propagation — the engine's walk from a changed cell to its dependents: mark stale, fire listeners, notify subscribers.
- CellValue — what a cell holds:
{ data, status, error, computedAt, effects }. The value knows its own freshness. - Adapter — external code that drives a sensor or io cell:
engine.push(id, reading)on its own event loop. - Dependency / dependent — the two sides of an edge:
is_darkdepends onambient.light;ambient.light's dependents includeis_dark. Both sets are explicit and inspectable. - Sheet — a YAML file: id, axes, cells. The executable artifact.
- Context — row, column, identity, trace, metadata, timestamp — everything a call knows about where it came from.
- Effect — a declared side effect a cell performed (network, storage, io, model, compute) — the audit trail on every CellValue.
- Status —
idle/computing/ready/error/stale: what the cell knows about its own value.
| Want to… | Go to |
|---|---|
| Use Quilt right now (stable, TypeScript) | This repo. |
| Try the browser simulator (live, no install) | superinstance.github.io/quilt/landing/simulator.html |
| Try Quilt Live (full reactive OS, single file) | superinstance.github.io/quilt/landing/quilt-live.html |
| Read the manifesto (the 10-point declaration) | docs/manifesto.md |
| Read the architecture deep-dive | docs/architecture.md |
| Read about security and the trust model | docs/security.md |
| Walk through the 5-chapter tutorial | tutorials/README.md |
| See 10 recipes for common patterns | docs/recipes.md |
| Compare to n8n, LangGraph, Observable, Excel | docs/comparison.md |
| Embed Quilt in Rust (single binary, embedded) | superinstance/quilt-rust |
| Get the same engine as a static binary (alpha) | superinstance/quilt-rust |
| Use the TUI (terminal view, tmux-friendly) | packages/tui/ |
| Try Quilt in a single HTML file (no install, portable) | superinstance/quilt-live — open one file, save state as cookie or downloadable .html |
| Report a bug | issues |
| Component | Status | Notes |
|---|---|---|
core engine |
✅ Stable | 9/9 unit tests passing. ~1,500 lines. |
mcp server |
✅ Stable | 5 tools + 1 resource per sheet. |
cli |
✅ Stable | init / run / serve / get / set / inspect / test. |
tui |
✅ Stable | Live cell grid, dependency overlay, key bindings. |
| Simulator | ✅ Stable | Browser, no build step. |
| Examples | ✅ 4 working | All four run end-to-end. |
| Templates | ✅ 3 working | Copy-and-customize. |
| Web UI | 🚧 Planned | v0.2 — WASM-compiled quilt-core. |
| WASM sandbox | 🚧 Planned | v0.2 — close the JavaScript sandbox gap. |
| Rust port | 49 tests passing; same engine, same model. |
A quilt is a stitched composition of small fabric pieces into a working whole. Each piece is itself complete, but the quilt is more than the sum.
The pieces are cells. The stitches are addresses. The whole is the runtime.
The name was chosen for an AI agent encountering it cold:
- One syllable. Easy to say, easy to type, easy to remember.
- Universal metaphor. Every culture has quilts.
- CLI-friendly.
quiltis a 5-letter command. No flags, no conflicts. - No major tech collision. No
quilt.combaggage, noloom.combaggage, notesserapretension.
Other names we considered and rejected: loom (loom.com video tool), tessera (3 syllables, Latin), mosaic (1990s browser), cellar (negative connotation), atlas (overused).
We welcome PRs that:
- Add a new cell evaluator
- Add an adapter (MQTT, Modbus, OpenAI, Anthropic, etc.)
- Improve the MCP tool list
- Add a Web UI
- Fix a security gap
- Port the Rust examples to TypeScript
- Improve the documentation
Open an issue first if you're planning something large.
Apache 2.0.
Use TypeScript when you want the full experience — browser, TUI, MCP, simulator, all the examples, all the docs. Use Rust when you need a single binary that ships anywhere. Both speak the same sheet format.
