Live
The real devtools, watching a real engine in your browser.
A shop flow of six neurons runs here every couple of seconds, and every fifth card is declined. This is the actual devtools panel, connected to it without a server: open a failed run and press Retry, and it resumes from the neuron that failed.
Resume
Fail it mid-run. It continues from where it stopped.
The run fails inside payment. Its frontier is the work still outstanding, and activating it runs only that branch: the stock is not reserved a second time. Checkpoint the same frontier and it survives a process restart too.
Isolated units
Each neuron owns one responsibility. Change one and the compiler and the graph show exactly what it affects.
Typed and exhaustive
Signals are typed collaterals, and the compiler makes you handle every one you subscribe to. An incomplete flow does not build.
Traceable and resumable
Every hop lands in devtools. A running flow persists its frontier and resumes after a restart.
Where it fits
Frontend orchestration
UI events become signals; neurons write the store, queries keep it equal to the server.
Backend orchestration
Services and handlers as one typed graph: order, concurrency, resume after a crash.
Backend jobs
Workers and queue-triggered flows: fan-out, fan-in, concurrency gates.
Integrations
Webhooks and third-party APIs as explicit, testable steps.
Pipelines
Step-by-step transforms with retries, backoff and clear boundaries.
Sagas
Deterministic retries, cancellation and compensation.
Frontend
Click a button. Can you say what happens next?
Developers say it in their own words: the answer is spread across reducers, middleware, effects and atoms, and the state manager cannot tell which of them changed a value.
“Sagas turned into an impenetrable spaghetti mess of actions and events going everywhere… it was impossible to trace what was going on.”— a Redux maintainer, Hacker News
In CNStra the flow is the graph: each neuron names what it reacts to and what it emits, and the compiler checks the wiring.
“Your action handling logic is split across thunks & reducers, and is hard to follow.”— Hacker News
One signal starts one stimulation, and its whole chain — async steps included — is one run you can read and replay.
“Currently, the only info about a change is when it occurred.”— Jotai issue #931
The devtools show every hop of a run: which neuron wrote what, from which signal, in which order.
“The component (and its children) have to re-render between each set call in the chain… Such code is often rigid and fragile.”— react.dev, You Might Not Need an Effect
The steps live in the graph, not in hooks, and a whole turn of it commits to the store in one frame.
In CNStra the flow is a graph you can read. A UI event is a signal; each neuron says what it reacts to and what it emits; the compiler checks the wiring, and the devtools show every hop — which neuron wrote what, and in which order. The store stays a store: it holds the data and redraws the screen.
Homeostasis
Read a key. The data is already on its way.
Like React Query or a GraphQL client, but with no query hook and no query key: a screen reads a slot of the store, and an empty slot asks for itself. The transport is any async function — REST, GraphQL, a socket — and you choose collection by collection what syncs on its own.
const cardReceptor = new CNSHomeostatReceptor<[cardId: string], TCard>({
name: 'card',
transduce: ([cardId], signal) => api.cards.get(cardId, { signal }),
});
const board = defineCNSHomeostatModel().collection('cards', {
idOf: (card: TCard) => card.id,
homeostatReceptor: cardReceptor,
});
const db = new OIMDB();
const { cards } = createCNSOIMDBHomeostatSchema(db, { homeostatModels: { board } }).board;
const cardSchema = exoJsx<{ status: TExoAttrPort; title: TExoTextPort }>($ => (
<article class="card" data-status={$.status}>{$.title}</article>
));
const cardMachine = composeExoMachine(
createExoMachine(cardSchema, {
createInitialState: (_c: TExoDomNode, cardId: string) => ({ cardId }),
}),
m => withOIMEffects(m, effect => {
effect(c => {
const card = cards.collection.getSlotByKey(c.state.cardId).entity;
const { status } = cards.homeostasis.getSlotByKey(cards.keyOf(c.state.cardId)).value;
c.attr(cardSchema.ports.status, status);
c.text(cardSchema.ports.title, card?.title ?? '');
});
})
);
The read is the request
The first screen that reads an empty card asks the receptor, on the next flush. No query hook, no query key, no loading flag to wire.
One request, many readers
Every screen reading the same card shares its slot and its request. With a batch transducer, the keys of one turn go out as one request.
Quiet when nothing changed
A refetch that answers the same card writes nothing, so nothing redraws. Loading and failure are the key's own status, right next to the data.
Built for reads. A write stays your signal: the model's neuron writes the change and invalidates what it touched. The Exodra JSX is compiled by its build plugin; the React component is plain React with @oimdb/react.
Wire it once. Watch it run.
Install @cnstra/core, declare a neuron, and stimulate it.
Recommended together with
CNStra decides what happens and in what order. OIMDB holds the data and reacts to it. One line connects them, and every turn of the orchestrator lands in one frame:
cns.addDrainListener(() => db.flush());Using CNStra with OIMDB →Exodra draws the screen. An event port of a machine becomes a signal of the CNS, and the screen reads what the neurons wrote into OIMDB:
receptor(Ports.RENAME, deckRenamed, payloadOf);Using CNStra with Exodra →