Mental model#

Four ideas carry the whole framework. Learn these and the rest of the docs is detail.

1. One definition, three projections#

A component({...}) is not a function that returns markup. It's a declaration the runtime projects three ways:

Projection Who consumes it Where it shows up
View Humans Server-rendered HTML, resumed on interaction
Resource Agents (read) ui://counter — plain JSON state + derived
Tools Agents and humans counter.inc with JSON Schema input + guard

The view is a projection, not the source of truth — which is why adding an intent instantly gives agents a new capability, and why the two audiences can never diverge.

2. State is typed, and only intents mutate it#

State is declared with schema, not inferred from initial values. Every mutation goes through a named intent whose input is validated — there is no setState you can call from anywhere.

state: schema({ count: int() }),
intents: {
  inc: intent({ input: schema({ by: int().default(1) }), run: ({ state, input }) => (state.count += input.by) }),
},

That restriction is what buys the agent surface: every state transition is named, validated, guarded and auditable — identical whether a human clicked or a model called. Direct writes outside a run() throw.

Corollary: run() receives everything it needs in its bag (state, input, ctx, emit, use). It must not close over outer variables to mutate state — the definition has to be fully describable, or the projection would lie.

3. Islands resume; they don't hydrate#

A page is server-rendered HTML. Components with state become islands: their state is serialized as a JSON snapshot, and the component's code loads on first interaction, restoring from that snapshot. Nothing re-renders on load, no closure is serialized, no effect is replayed. A page with no islands ships 0 KB of JavaScript.

Mark an island eager when it must be live on arrival (an editor, an event listener), and persist when its live instance must survive navigation.

4. Guards are the contract with agents#

Each intent (and each api()) declares who may run it and whether a human must approve:

Guard Human Agent
auto (default) runs runs
confirm runs returns a proposal a human approves
forbidden runs rejected

Guards resolve per request origin, server-side. This is how you let a copilot fill a cart but never check out unattended.

Where things live#

Islands are ephemeral, stores and the server are durable. State that must outlive a navigation belongs in a store (or on the server), not in a route island. Put it there and it survives; leave it in a route island and it re-resumes fresh from the incoming page — by design, not by accident.

Coming from React?#

React Janux
useState + setters state: schema({...}) + intents
useMemo / derived render derived: { total: (s) => ... }
useEffect + deps array effect({ when: (s) => s.items, debounce: '300ms', run })
Context / a store library store({ name, scope: 'app' }) + use: { theme }
Data fetching hooks source({ query, refresh: every('5m') }) or useQuery
<Link> a plain <a href>navigation is automatic
An MCP integration you write generated

Real React components still work — mount them unchanged with foreign() when you need the ecosystem.

Next: Editor setup · Components