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
subtotal -> 37.50 subtotal -> 60.00 final subtotal: 60.00

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

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.