◆ Flux

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.

The four planes 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.

FLUX
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.

FLUX
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
FLUX
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:

FLUX
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 either

live(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:

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 formal rules → This chapter narrated four planes and the firewall between them. Their precise definitions live in the Spec, FVM and FDK: