5-Minute Quick Start
This page is the impatient developer's path: install the
engine, write one file, run it, see output. Five minutes from
cold start to a running graph that recomputes derivations and
fires a subscriber. No React, no bundler, no scaffolding — just
node, TypeScript, and fifteen lines of code. If
you want the longer walkthrough with each concept introduced
in context, see Your First App;
if you want to know why the API is shaped this way,
see Why Causl.
Step 1 Install
You need Node 20 or newer and a package manager. The engine itself has zero runtime dependencies, so the install is a few hundred kilobytes.
mkdir causl-quick-start && cd causl-quick-start
npm init -y
npm install @causl/core
npm install -D typescript tsx @types/node
Add a minimal tsconfig.json so tsx is
happy with the ESM exports:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
Then set "type": "module" in
package.json — causl ships ESM-only, like the
rest of the modern Node ecosystem.
Step 2 Write the graph
Create quick-start.ts. The whole working example
is below; we will walk through it after you have it running.
Step 3 Run it
npx tsx quick-start.ts
That's it — installed, written, and running. The rest of this page is the line-by-line explanation; if you are happy to poke at the code yourself first, skip to Core Concepts.
What just happened
Five things, in order. Each one is a deliberate API shape, not an accident:
1. createCausl() returns a graph
A graph is the engine's transaction context — it owns the
input cells, the derivation registry, the commit log, and the
subscriber list. You can have many graphs in one process; they
do not share state. The name option is optional;
omitting it mints a UUID. Naming is helpful for devtools and
for the structured-error messages the engine emits.
2. graph.input registers a mutable cell
An input is the only kind of node you can write to. It carries
a name (used in errors, devtools, and the exported IR) and an
initial value. The returned handle is opaque — you read it with
graph.read(node) and write it with
tx.set(node, value) inside a commit. There is no
direct mutation surface and no setter on the node itself, which
is the structural guarantee behind the
transactions are the only mutation boundary
commitment.
3. graph.derived registers a pure projection
A derivation is a function of other nodes. The
get callback inside it tracks reads — so
subtotal automatically depends on price
and quantity, and only those. The engine builds the
dependency edge from the read, not from a declaration; if you
change the function later and read different nodes, the edges
change with it. Derivations are recomputed in topological order
once per commit (SPEC §5.1, phase D), never partway through, so
a single derivation never observes an inconsistent slice of the
inputs.
4. graph.subscribe observes
The subscriber fires after the commit lands and only if the
observed node's value actually changed. The third
commit in the example sets quantity
to 4 when it was already 4 — the
subscriber does not fire, because the engine compares the new
committed value against the previous one with referential
equality (or a custom equality function, if you supply one).
This is the per-commit equality cutoff, and it is what makes
"subscribe to a derivation that fans out from one input" cheap.
5. graph.commit is the only way to mutate
Every write goes inside graph.commit(intent, tx => ...).
The intent is a human-readable label that lands in
the commit log; the tx handle exposes
tx.set. If the callback throws, no write is
published — the graph stays at its previous state. If it
returns, every write in the transaction is published atomically
and every affected derivation is recomputed in one pass before
any subscriber sees anything. This is what
atomic-or-nothing means in practice.
Detailed explanation: the eight-phase pipeline.
Each commit runs through eight phases (A–H) the SPEC calls out
explicitly: precondition checks, input writes, an input-layer
fast path, the Kahn topological recompute, frozen
Commit assembly, append to the commit history,
subscriber dispatch, and transient subscriber cleanup. As a
caller you never see the seams; the only observable contract is
"all writes land together, subscribers fire after." See
SPEC.md §5.1 for the full pipeline.
What to try next
-
Add a second derivation —
graph.derived('withTax', g => g(subtotal) * 1.08)— and subscribe to it. Notice the diamond: two derivations from one input still fire exactly once per commit. -
Pass
{ explain: true }as a commit option, then inspectgraph.explain(subtotal)after a commit — you will see the upstream reads that fed the value. -
Throw an error inside a
commitcallback and confirm thatgraph.read(price)still returns the pre-commit value. - Skim the playground — the same example runs in a Monaco editor against a live engine, so you can edit the derivation and watch the subscriber fire in real time.
When you are ready to wire this into a real UI, head to Your First App for the React version, or jump to the Tutorial for a complete application with persistence, async resources, and the static checker enabled.