Commit once.
Observe coherence.

Causl is a state engine for applications whose model is a live graph of facts whose derivations cascade. Atomic commits, automatic dependency tracking, dynamic-dep cleanup, glitch-free diamonds, and pre-runtime race detection — held by property tests and a bounded model checker, not by promises.

Engine@causl/core · TypeScript
Race detectioncausl-check · causl-enumerate
LicenseMIT
import { createCausl } from '@causl/core' const graph = createCausl() const a = graph.input('a', 1) const b = graph.input('b', 2) const sum = graph.derived('sum', g => g(a) + g(b)) const sum1 = graph.derived('sum+1', g => g(sum) + 1) graph.subscribe(sum1, v => log(v)) // 4 graph.commit('bump-a', tx => tx.set(a, 10)) // 13 graph.commit('bump-both', tx => { tx.set(a, 100); tx.set(b, 200) }) // 301 — exactly one notification, not two
Why this exists

State that is not a tree of values.

Spreadsheets, CMMS, BIM-style asset graphs, capital planning tools, scheduling systems, scenario planners, configuration editors, large operational consoles — applications whose state is a live graph of facts whose derivations cascade.

Cascade

One action invalidates dozens.

A single user edit fans out through derived values, and some dependencies change which inputs they read as the user navigates. Static selector graphs can’t express this; manual memoisation drifts as the model grows.

Async drift

Stale results outlive their context.

A fetch returns after the dependency it was launched against has already moved. AbortController is part of the answer; the rest is treating freshness as a typed property of the result, not an ad-hoc if in the resolver.

Glitch

Wrong order is data corruption.

Re-renders that fire in the wrong order produce visible-but-inconsistent intermediate frames. When that state is also persisted, the bug is not a render bug — it’s corruption that ships to disk and to other users.

What causl does differently

Eight commitments shape the library.

Each commitment names an unavoidable engine concept; together they bound what the public API is allowed to grow into.

  1. Denotational semantic foundation.

    A derived value’s meaning is a function of its inputs at a given commit time: Behavior a = GraphTime → a. Glitch-freedom is then a theorem, not a scheduler trick.

  2. Transactions as the only mutation boundary.

    All writes happen inside graph.commit(intent, tx → …). Outside, the graph is read-only. There is no concurrent-write API to misuse.

  3. Dependency tracking with deterministic cleanup.

    A derivation that today reads assetA and tomorrow reads assetB no longer fires on assetA writes — proven by property tests, not promised by docs.

  4. One composite statechart for every lifecycle.

    Resource fetch, conflict status, transaction phases, and interaction modes share one chart with shared event vocabulary. No more parallel string enums sprinkled across object fields.

  5. Strict layering.

    The user’s information model, the editor’s controller state, and the engine’s substrate live in separate identifier namespaces and separate packages — enforced at the package boundary, not in the docs.

  6. Discriminated-union state everywhere.

    Optional fields that hide state machines are forbidden. Impossible states cannot be represented; the type checker is the first reviewer.

  7. MVU-shaped application surface.

    A typed Msg union dispatched through update : Msg → Model → Commit. Transactions are the engine room; messages are the front door.

  8. Pre-runtime race detection in CI/CD.

    Two Rust-backed CI tools ship today: causl-check (twelve-pass static IR linter) and causl-enumerate (SPEC §16.4 bounded model checker, with an Apalache differential runner against the EPIC-7 TLA+ corpus).

How it compares

Where causl is the right tool — and where it isn’t.

Honest about where existing libraries are strictly better (), where they cover the concern in some form (~), and where the concern is missing (). The Causl column uses for what currently ships and for in-flight or planned future work.

Concern Redux + RTK MobX Jotai Recoil Zustand Valtio TanStack Query XState Causl
Transactional commits (atomic write boundary) ~ ~ ~
Automatic dependency tracking on reads ~ ~
Dynamic-dep cleanup proven correct n/a ~ ~ ~ n/a ~ n/a n/a
Glitch-free diamond as a guarantee ~ ~ ~
Denotational semantic specification ~
Composite statechart for all lifecycles
Stale-async protection by version ~ ~
Conflict records as queryable state ~
Discriminated-union state (impossible states) ~ ~ ~ ~ ~
Strict model / controller / engine layering ~ ~
MVU-shaped typed Msg dispatch ~
Pre-runtime race detection in CI/CD ~
Live derivation editing in devtools ~ ~
Spreadsheet-grade dependency cascades ~ ~ ~
Excellent at: small global state ~ ~ n/a ~ ~
Excellent at: server cache / fetch dedupe ~ ~ ~ ~ ~
Excellent at: hierarchical UI state machines ~

Legend: strictly handles · ~ covers in some form · does not address · future goal · n/a not in scope. For the full reading-the-table notes, see the README.

When to reach for causl

Built for the cases that have stopped being solvable as a stack of libraries.

Causl is over-engineered for simple apps and the only way to ship the complex ones without losing your mind. Pick the right tool.

Reach for causl when two or more are true

  • State is a graph; one action cascades through dozens of derived values.
  • Derivations change what they depend on as the user navigates.
  • Async fetches may be stale by the time they return.
  • You need an audit trail of every state change with a typed intent.
  • Conflicts must survive the transaction that created them — as data.
  • Spreadsheet-style cells with formula references, or asset hierarchies with reference-based dependencies.
  • You want race conditions caught in CI before they reach production.
  • A bug in propagation is data corruption, not a UI glitch.

Reach for something else when

  • State is a flat object with twenty fields and no cross-field derivations — Zustand or Jotai.
  • State is mostly cached HTTP — TanStack Query (or Apollo / Relay).
  • The problem is one big form with validation — React Hook Form.
  • The problem is a wizard with five steps and a back button — XState directly.
  • You want a library you can adopt incrementally without thinking about the model layer. Causl asks you to commit to layered architecture (information model vs. editor controllers vs. engine substrate).
Packages

The causljs/causl-ts workspace at a glance.

Each package owns one concern. Adopters install only the pieces they need; the engine has no React dependency. Six sibling repos round out the cross-org topology (Rust engine, WASM bridge, TS fork that defaults to it, bench harness, static analysis, this site) — see the documentation page.

@causl/core
Engine — Behaviors, derivations, transactions, snapshot/hydrate, retention, explain.
@causl/react
React bindings — useCausl, useDispatch, useCauslFamily, MVU runner, SSR.
@causl/formula
Spreadsheet patterns on top of the core — formulas, ranges, cycles.
@causl/sync
Async resources + conflict registry as composed statecharts.
@causl/devtools
Inspection primitives — explain materialisation, liveDerivation, snapshot, statechart.
@causl/devtools-bridge
Redux DevTools Extension protocol bridge (zero-cost when absent).
@causl/persistence
Persisted-input adapter with structured PersistenceError reporting.
@causl/checker
npm wrapper for causl-check — the Rust-backed static IR linter (twelve passes).
@causl/bench
Benchmarks — Jotai / RTK / MobX comparisons across the canonical scenario taxonomy.
@causl/migration-check
Migration drift detector — flags unmigrated Jotai/MobX/Redux patterns in adopters.

Two Rust crates ship in causljs/causl-check as CI gates, not runtimes: causl-check (static IR linter, now with adopter-defined race classes per RFC 0001 and federated cross-repo runs per RFC 0002) and causl-enumerate (SPEC §16.4 bounded model checker, with an Apalache differential runner against the EPIC-7 corpus and a Tier-3 Apalache TLA+ corpus for the SPEC.async §9.1.1 S-rows).

Status

Phases 1–4 ship on main. Pre-1.0.

The Phase-8 SPEC compliance audit is closed; the bounded enumerator’s full §16.4.1 type surface is implemented. APIs are stable but not version-locked until 1.0.

Shipped

Semantic core

Atomicity, glitch-freedom, dynamic-deps, replay determinism, cycle detection — held by 1000-trial property suites in packages/core/test/properties/.

Shipped

React surface

useCausl, useDispatch, useCauslFamily, Suspense + SSR — tested under StrictMode mount/unmount cycles.

Shipped

Race detection toolchain

causl-check + causl-enumerate run in CI against the spreadsheet and async demos. docs/apalache-diff-report.md regenerates on every CI run.

Shipped

Bounded enumerator

Full SPEC §16.4.1 surface — 10-field State, 8-arm Action, transition_phased with per-step events + phases, Oracle::check, Tier-1/2/3 Bound presets, enumerate_with_script entry point. 43 enumerator test binaries.

Shipped

BFS memory ceilings

Configurable via CAUSL_BFS_FRONTIER_CAP / CAUSL_BFS_TRACES_CAP / CAUSL_BFS_RACES_CAP env vars. Conservative defaults stay until adopter empirical data supports retuning.

Shipped

Hosted playground & spreadsheet

The Monaco playground and 100-cell spreadsheet demo ship as static HTML pages under causl-org/. Both load @causl/core, @causl/formula, and @causl/devtools live from esm.sh — no build step required.

Quickstart

The smallest worked example.

SPEC §10 — two inputs, one derived value, one diamond derivation, one subscriber, two commits, three observed propagations. Pinned as an acceptance test in packages/core/test/spec-10-worked-example.test.ts.

# prerequisites: Node 24.x + pnpm 10 pnpm add @causl/core pnpm add @causl/react # React bindings (optional)

The repository pins Node via .nvmrc and pnpm via packageManager in the root package.json. Run pnpm validate before committing — it runs typecheck + build + test, the same toolchain CI runs.

import { createCausl } from '@causl/core' const graph = createCausl() const a = graph.input('a', 1) const b = graph.input('b', 2) const sum = graph.derived( 'sum', g => g(a) + g(b) ) graph.commit('bump-both', tx => { tx.set(a, 100) tx.set(b, 200) }) // Subscribers fire exactly once.

Ready to read deeper?

The documentation bundles the rendered SPEC, per-package API references generated from the source, and the changelog. The repository carries the eight-commitment audit trail.

Browse docs