The four planes
A Flux program is not one kind of thing. Computing an indicator, drawing a comet that follows the price, morphing a chart when the asset changes, and running a stateful application in a pane are four different activities — four different clocks, four different sets of rules. Most languages hand you one plane and ask you to be careful across all four. Flux gives you four planes and makes the boundary between them a property of the type system.
You never pick a plane; you write the maths and the plane is inferred from what you wrote. And because the boundary is typed rather than conventional, one sentence holds the whole design together and is worth reading twice: a value can be animated, random, and frame-dependent, and still be provably incapable of changing a computed number. Not by convention. By construction.
Figure — the four planes and the one-way firewall.
Before the examples, one thing about the samples on this page. Flux has no expression-statements,
so a bare expression is not a program. The lines marked ✗ below are therefore expression
fragments: they exist to show what the kind rules refuse, not what the parser accepts. Every
unmarked line is a legal statement.
ANALYSIS — the plane that computes
| Clock | the bar — it advances only on closed data |
| Guarantees | total, causal, deterministic, sandboxed, no-repaint |
| What lives here | indicators, signals, representation transforms, the numbers a decision rests on |
Analysis is the most constrained plane, and therefore the most trustworthy. Everything in it is a pure function of the past: delays reach backwards only, resampling reads only closed units, feedback must cross a unit delay. What follows from those rules is not a promise but a theorem — a value, once produced for a bar, can never change.
plot rsi(close, 14)
plot ema(close, 20) @ tf("1h") // a coarser clock — still causal
mark close cross_up ema(close, 50)That is the whole plane: sinks (plot, mark, later alert and assert) publishing pure
series to the host. Notice what is absent. Analysis reads nothing from the planes above it —
there is no symbol here for the mouse, for the wall clock, for the current frame, or for whether
3-D mode is on. This is not “bad practice you should avoid”: those names do not exist in the
analysis namespace at all. That absence is what makes the numbers replayable.
CANVAS — the plane that shows
| Clock | the frame |
| Allowed | screen space, wall-clock time, randomness — explicitly outside the guarantees |
| What lives here | scenes, animated drawings, decoration, effects |
The canvas plane may read analysis. It may not write it. Here you get everything the analysis plane forbids — the pointer, wall-clock time, randomness — because none of it can flow the other way.
circle {
at: (bar.i, spring(close)), // reads analysis; the easing is cosmetic
glow: 16 + 8 * throb(0.4), // per-frame, time-only — the compositor owns it
trail: 24
}
on close cross_up highest(close, 250)[1] -> burst(40) ring { at: (close), life: 2s }Every property is a signal — a constant, a data value and an animation are the same kind of thing here, so there is no separate animation API to learn. And because the compiler knows which signals are time-driven, it routes them to the host compositor rather than re-running your script each frame: zero JavaScript per frame for the parts that move the most. The scene geometry and text both render on the GPU, which sidesteps DOM layout cost; text is an SDF glyph atlas, crisp at any zoom. It is invisible to you — you write one scene — and it is detailed in display.
Everything a program shows — here on the canvas, or through the interface the APP plane paints —
reaches the page as an island: a <div> the real engine renders inside an ordinary static
HTML shell. Visual richness inside it is a closed set of typed style props the host validates
and applies, never a raw CSS string, so the one injection surface a UI language usually exposes is
not filtered but structurally absent. → the islands model
TRANSITION — the plane that interpolates
| Clock | the frame |
| Rule | it interpolates the rendering between two already-computed states |
| Consequence | it is cosmetic by definition — it cannot change a value, so it cannot repaint |
on switch(asset) -> morph chart over 500ms { ease: inOutCubic ; stagger: 0.3 }
on click -> focus(view, at: (bar.i, close), zoom: 2.0, over: 600ms)A transition takes two states that analysis has already computed and animates the rendering
from one to the other. Split it in two and the safety becomes obvious: a transition’s settle
value — where it lands — is analysis data and lives in the oracle; its trajectory — how it
gets there — is cosmetic and does not. That is exactly why prefers-reduced-motion can jump
straight to the end state and change nothing that any verdict depends on.
APP — the plane that remembers
| Clock | events |
| Shape | a bounded Model, a pure update, a pure view, declarative subs |
| Effects | inert command data the host executes, under default-deny capabilities |
The APP plane is sealed in design and additive to the core, and its rollout follows the v1 language.
The other three planes cannot hold state that persists between events and decides what is displayed — a score, a document, a selection, a blotter of open positions. The APP plane adds exactly that, and pays for it with a strict recipe: everything ambient — time, input, randomness, the network, analysis values — arrives as a message, and the message journal is the single source of truth. That is what makes an application replayable, testable without a mock, and re-executable by a server bit-for-bit. The full contract lives in the App plane.
The firewall
One rule governs all four planes:
Dependency arrows never point toward a weaker guarantee.
Presentation may read analysis. Analysis may never read presentation. The APP plane may read analysis (read-only, through a typed subscription) and may orchestrate presentation (through commands) — but it may never write analysis either.
APP (mutable state + effects) ← the most permissive plane
│ reads ANALYSIS (Sub OnSeries) ✔ read-only
│ orchestrates CANVAS / TRANSITION (Cmd) ✔
▼
CANVAS / TRANSITION (cosmetic, per frame) ← reads ANALYSIS ✔
▼
ANALYSIS (pure, causal, no-repaint) ← reads nothing above it ✘What the firewall forbids is precise. These are the values that may never flow into analysis:
| Forbidden in analysis | Why |
|---|---|
screen.*, hover, the pointer |
screen space is not data; it varies per device |
now(), the wall clock |
it is not replayable, and it would make a past value depend on when you looked |
unseeded rand, noise |
non-deterministic ⇒ two engines disagree |
live(e) |
it reads the forming bar — the one thing that can still change |
All four raise [ErrFirewall], at compile time, with an explanation rather than a scolding. The
check is not in the grammar — a single grammar parses all four planes — it is a dependency
analysis run on the parsed tree, so the diagnostic names the exact value that crossed the exact
line (see one grammar, all planes).
Why a plane split rather than a lint? A discipline you have to remember is a discipline you will forget at 2 a.m. under a deadline. A firewall enforced by the kind system is one you cannot forget: the name is not in scope, and the compiler will not let the value cross. That is what makes it safe to run a stranger’s animated, random, interactive scene right next to the number your decision rests on.
live() — the one exception, and why it is safe
You met live() in Guide §6 as the forming unit of a clock; here is why the firewall lets it
exist at all. Traders want to see an indicator update within the forming bar. That reading is
genuinely useful and genuinely non-causal — so instead of banning it or letting it leak, Flux gives
it a name, a plane, and a wall:
plot live(ema(close, 20)) // ✓ display — the forming bar included, per frame
alert live(ema(close, 20)) > 100 // ✗ [ErrFirewall] — a decision may not read a forming value
rsi(live(close), 14) > 70 // ✗ [ErrFirewall] — analysis may not consume one eitherlive(e) re-evaluates the analysis sub-graph of e including the bar in formation, per frame.
Its result may flow only into display sinks (plot, mark, fill, color bars, a scene).
Any confirmed sink — an alert, an assertion, a value a calculation consumes — is [ErrFirewall].
Where you place live matters, because the two placements are not variations on a theme. It
wraps the expression whose sub-graph is to be re-evaluated: live(ema(close, 20)) asks for
the average including the forming bar, and lands in a display sink. Pushed inward, onto a
kernel’s argument — ema(live(close), 20) — it stops being a display request and becomes a
forming value handed to a calculation, which is the breach itself. The firewall does not care that
a plot is waiting at the far end: the analysis kernel already consumed it.
Three consequences, all of which matter:
- The confirmed series of
estays byte-identical.live()adds a provisional view; it does not modify what was computed. live()is excluded from the byte-identity oracle, exactly as the wall clock is — so the guarantee that the two engines produce the same numbers is untouched.- A script that uses it is flagged non-replayable in the guarantees panel. You see the trade-off you made.
That is the general shape of every escape hatch in Flux: name it, bound it, wall it, and show the user what it cost.
Which plane am I on?
You never declare one. The plane is inferred from what you write — that is the point of “write the maths, the machinery follows”:
| You write | The plane |
|---|---|
plot, mark, fill, alert, assert, an indicator expression |
ANALYSIS |
a primitive with props, on … -> …, scene{…}, group, repeat |
CANVAS |
morph, focus, replay |
TRANSITION |
an app block |
APP |
A single file mixes them freely — an indicator and its presentation are one program — because the plane comes from the constructs, not from a mode switch at the top of the file. And if you try to mix them in a way the firewall forbids, the compiler tells you which value crossed which line, and what to do instead.
See also
- The four planes & the firewall — the concept, in lookup form.
- The App plane — the full application contract.
- The Canvas plane — signals, spaces, events, primitives, performance.
- Time & state — causality, clocks,
live()in depth. - Guarantees — what each plane promises, and how it is verified.
- display — the render strata, and where presentation determinism ends.
The formal rules → This chapter narrated four planes and the firewall between them. Their precise definitions live in the Spec, FVM and FDK:
- The plane inference and the one grammar — Grammar › One grammar, all planes, with the per-plane statement forms (Analysis sinks, Canvas, Transition, the App descriptor).
- ANALYSIS — causality, clocks,
live()and the oracle — Time & state.- CANVAS — signals, spaces, primitives, the performance model — The Canvas plane.
- TRANSITION —
morph,focus,replay, the transition descriptor — The Transition plane.- APP — Model, Msg, commands as inert data, subscriptions, capabilities — The App plane.
- The firewall as a concept, and
[ErrFirewall]— The four planes & the firewall.- Where presentation determinism ends (the WebGPU render target) — display.