value
formula
program
sensor
api
listener
router
io
value
A static, named piece of data. The simplest cell. The atom of the spreadsheet.
value kind: value
The reactive primitive of constants. Mutable from outside, but doesn't recompute from inputs.
Mental model
A value cell is a named box that holds a piece of data. It's reactive in that other cells can listen to it, but it doesn't have inputs of its own. It's the leaf of the cell graph.
Syntax
- id: temperature kind: value value: 22.5 - id: user.name kind: value value: "Alice" - id: enabled kind: value value: true - id: tags kind: value value: ["urgent", "frontend", "ux"]
When the value changes
Changing a value cell cascades through every cell that depends on it. The order of writes doesn't matter; the engine handles the topology.
Use cases
What it is NOT
A value cell is not a constant. It can be changed from outside (by user input, by an external system, by a listener). The difference from a formula is that it has no expr β it doesn't recompute; it's a leaf.
formula
A reactive expression. The core of the reactive system.
formula kind: formula
A pure expression that recomputes whenever any of its inputs change. Sync, deterministic, no side effects.
Mental model
A formula cell is a pure function from its dependencies to its value. When any dependency changes, the formula recomputes. It's the workhorse of the reactive system.
Syntax
- id: fahrenheit kind: formula expr: "sensor.temp * 1.8 + 32" - id: status kind: formula expr: 'heat_index > 30 ? "hot" : "ok"' - id: average kind: formula expr: "(a + b + c) / 3"
How it works
What you can use
The expression is a small JavaScript-like language. Numbers, strings, booleans, arrays, objects. Arithmetic, comparison, ternary, function calls (Math, JSON, Array.prototype). No statements β just an expression.
// All of these are valid: expr: "a + b" expr: 'status === "ok"' expr: "Math.sqrt(x * x + y * y)" expr: "items.filter(i => i.active).length" expr: "JSON.stringify(data)"
Use cases
Limitations
No async. No side effects. No `await`, no `setTimeout`, no `fetch`. For those, use program. A formula must be a pure expression β its output is determined entirely by its inputs.
program
An async expression. The escape hatch from pure formulas.
program kind: program
A small async body that can await, call other cells, and produce a value. Use when formulas aren't enough.
Mental model
A program cell is a function that can do work. It can call other cells, await async operations, throw errors, and return a value. It's the bridge between the synchronous, pure reactive core and the messy outside world.
Syntax
- id: summarize
kind: program
code: |
const items = runtime.get('raw_data').data;
const filtered = items.filter(i => i.score > 0.8);
const summary = await runtime.call('api.llm', {
prompt: 'Summarize: ' + JSON.stringify(filtered)
});
runtime.set('summary_text', summary);
return summary;
The runtime API
Inside a program cell, you have access to a runtime object:
runtime.get(id) // β { data, status, error, computedAt }
runtime.set(id, value) // set another cell
runtime.call(id, args) // β await a program or api cell
runtime.cells // β a Proxy of all cell values
runtime.log(...args) // β log to the engine console
Use cases
When to use a program vs a formula
Use a formula when the computation is pure and synchronous. Use a program when you need await, side effects, or want to set other cells. Programs are the escape hatch β use them when needed, not by default.
sensor
A polled input source. The cell that reads the world.
sensor kind: sensor
A cell that holds the latest reading from an external source. Polled, scheduled, or pushed.
Mental model
A sensor cell is a window into the world. It's the cell that says "the temperature is X right now" or "the user is on page Y" or "the timer has fired N times". The engine doesn't know how the value is updated β it just holds the latest reading.
Syntax
- id: sensor.temp kind: sensor source: dht22 default: 22 poll_ms: 1000 - id: sensor.motion kind: sensor source: pir default: false - id: sensor.clock kind: sensor source: timer interval_ms: 60000
How it works
The engine doesn't poll directly. Instead, adapters push values into the sensor cell. The cell then re-evaluates, and any formula that depends on it recomputes.
Sources
A sensor's source is an identifier for an adapter. Each runtime (TypeScript, Rust, Live) defines its own set of sources. Common ones:
source: dht22 // temperature/humidity sensor (ESP32) source: pir // motion sensor source: timer // clock-based source: ble // Bluetooth Low Energy source: mqtt // MQTT subscription source: websocket // WebSocket source: interval // client-side timer source: file // file watcher
Use cases
api
An outbound call. The cell that talks to the world.
api kind: api
A cell that calls an external service. Lazy, cached, addressable. Like a function over the network.
Mental model
An api cell is a named, addressable network call. You reference it by id; the engine makes the HTTP request; the result is cached. The cell is the function; the network is the implementation.
Syntax
- id: api.users
kind: api
endpoint: "https://api.example.com/users"
method: GET
- id: api.weather
kind: api
endpoint: "https://api.weather.com/v1/current"
method: GET
params:
lat: 37.7749
lon: -122.4194
cache_ttl: 300 # 5 minutes
How it works
API cells are lazy. They don't fetch until something reads them. When read, the engine calls the endpoint, caches the result (per the cache_ttl), and returns the value. Subsequent reads (within the TTL) return the cached value without re-fetching.
Per-context memoization
The same api cell called from different formulas can have different cached values, depending on the caller's context. The engine tracks the caller and respects the cache accordingly.
Use cases
listener
Fires on change. The cell that reacts to the world.
listener kind: listener
A cell that watches another cell and fires an action when a condition is met. The "if X then Y" primitive.
Mental model
A listener cell watches a target cell. When the target changes, the listener evaluates a condition. If the condition is true, it fires an action. The action is a side effect β log, alert, HTTP call, anything.
Syntax
- id: alert.hot
kind: listener
watch: sensor.temp
condition: "sensor.temp > 30"
action: "console.log('β οΈ Hot!')"
- id: audit
kind: listener
watch: critical.value
condition: "true"
action: |
runtime.set('audit_log', [...runtime.get('audit_log').data, {
who: 'system',
what: 'critical.value',
from: oldValue,
to: runtime.get('critical.value').data,
when: Date.now()
}])
How it works
Use cases
Listeners vs formulas
A formula computes a value. A listener fires a side effect. The distinction: formulas are pure; listeners are not. Use formulas for values, listeners for actions.
router
Context-aware dispatch. The cell that decides.
router kind: router
A cell that returns different values based on who is asking. Multi-tenant, role-based, A/B tests.
Mental model
A router cell is a single cell with multiple possible outputs. The output depends on the caller. The same cell id, different values for different callers.
Syntax
- id: data.public
kind: router
routes:
- when: "caller.role === 'admin'"
expr: "all_data"
- when: "caller.role === 'user'"
expr: "filtered_data"
- when: "caller.role === 'guest'"
expr: "public_data"
default: "public_data"
How it works
When something reads the router cell, it provides a caller context. The router evaluates each route's when clause; the first match wins. The result is the cell's value for that caller.
Per-context memoization
Each caller gets their own cached value. The same router cell, called from two contexts, evaluates twice and caches twice. This is what makes routers work without leaking data between callers.
Use cases
io
A physical port. The cell that drives the world.
io kind: io
A cell that drives a physical actuator. LED, relay, motor, pin. The output of the cyber-physical loop.
Mental model
An io cell is the end of the cell graph, in the physical world. It writes to a port β GPIO pin, serial, PWM channel, motor controller. The cell is a name for a physical output.
Syntax
- id: led.green kind: io port: "gpio2" direction: out - id: motor.fan kind: io port: "pwm0" direction: out - id: relay.heater kind: io port: "relay1" direction: out invert: false
How it works
When a formula writes to an io cell, the value is dispatched to the port driver. On an ESP32, that means setting a GPIO pin or a PWM duty cycle. On a server, it could mean calling a hardware API (a Modbus device, a serial port, a DAQ).
Use cases
How they compose
The 8 cell kinds are a complete reactive system. Any non-trivial sheet is a graph of them.
A real system is rarely one kind. A weather station is sensor cells (DHT22, motion, light), formula cells (heat index, comfort, threshold), listener cells (alert on heat), and io cells (drive the fan, light the LED). The 8 kinds compose into anything.