◆ Flux

What Flux deliberately doesn’t do

The discipline that makes Flux safe is the same discipline that bounds what it is for. A language that provably terminates cannot also run an open-ended search; a runtime with no I/O primitives cannot also be a filesystem. None of that is a gap waiting to be filled. Each limit below is a decision with a rationale, and naming it plainly is part of the promise — a trust page that lists only strengths is a sales page.

So this chapter is the honest other half of the tour. You have seen what Flux does with a line, a scene, and an application; here is what it refuses to do, why the refusal is the feature, and — every time — what you write instead.

No free-running loops

Flux is total: every program provably terminates. By definition that makes it not Turing-complete, and it is worth saying out loud rather than hiding. The trade is precise and deliberate: a program is unbounded over time, bounded per step. A stream runs forever, one value per new unit; the work inside each step is a finite dataflow graph whose every loop, window and fold carries a compile-time constant bound under a cap.

You met the shape of this already — windows take constant lengths:

FLUX
plot ema(close, 20)               // ✓ a constant window
// ✗ plot ema(close, len)         — [ErrTotal]: a window bound must be a compile-time constant

A program that cannot state its bound is rejected at compile time with [ErrTotal], rather than started and killed by a timeout later. When you genuinely need iteration — Newton’s method, a relaxation step — you write loop(max, …) with the cap declared up front: twenty steps, said in the source, not “until it converges.” That is exactly what careful numeric UI code does anyway; the difference is that Flux makes the honesty mandatory.

This is the synchronous-dataflow model of Lustre, Esterel and SCADE — the languages that run avionics and railway signalling, total for the same reliability reasons. And the constraint is nearly invisible here, because of where Flux runs. An unbounded inner loop in a client-side render path is a hung tab: a bug, not a feature you were reaching for. The rare things that truly need open-ended iteration — a general solver, an interpreter, a SAT search — are precisely the things you would never put on the UI thread. They belong in a backend, which is where Flux’s other limits already send you.

No direct I/O from a script

The language has no fetch, no DOM, no eval, no file handles, no sockets. A script cannot reach the outside world at all — it can only describe an effect and hand the description to the host. Effects are inert command data; the host, and only the host, executes them under capabilities you granted.

You see this most clearly in the APP plane, where update returns new state plus a list of commands it does not run:

FLUX
update(m, Save) = { model: m, cmds: [ Persist("doc", m.doc) ] }   // inert data — the host writes it

Persist(…) is a value, like 3 or "hello". Nothing happens until the host reads it, checks the capability behind it, and performs the write. This is what turns running someone else’s code from a risk assessment into a routine act: a shared or purchased artifact arrives as WASM only, never source, carrying an inspectable manifest of every capability it may ask for — aggregated transitively, so a buried dependency cannot hide its appetite. It is total, so it cannot run away; sandboxed, so it has no I/O to abuse; and default-deny, so it does exactly nothing you did not grant.

AI-generated code is one compelling instance of this. The guarantees target untrusted authors in general — they do not care whether a human or a model wrote the binary — and a generated indicator is covered by the same manifest, the same sandbox, the same totality as any other. It is a case the design already handles, not a separate feature.

The public sharing and marketplace rollout follows v1. The trust model that makes it safe — capabilities, totality, no-repaint, the WASM-only artifact — is v1 and is not weakened anywhere.

The bar is the time grain

The ANALYSIS clock advances on closed data units — bars. The whole live infrastructure is bar-streaming, and live() reads the forming bar, per frame, for display only. There is no tick-by-tick or order-flow scripting in v1.

Sub-bar granularity is a named extension whose rollout follows v1, a scope boundary drawn on purpose with a mechanism already behind it — not an oversight, and not an implicit promise you should design around today.

External data comes in through the front door

Network-fed data — funding rates, open interest, a macro series — enters the APP plane through a typed subscription. It cannot silently become an indicator, because the firewall forbids an application from writing into ANALYSIS:

FLUX
subs(m) = [ OnSeries("funding-rate", GotFunding) ]   // arrives as a message, never as a plot

To become an indicator, a stream must first be folded into closed bars and handed over through the data:source seam, which the host ingests append-only — a closed bar is never revised, so no-repaint survives the crossing.

An indicator driven by a non-price series is not expressible in v1. The metric kind names the seam through which such a stream could enter ANALYSIS — designed, held open, and inert, so admitting one later changes no grammar. The limit is named rather than hidden.

A handful of instruments, not the whole market

Cross-series work over a few named instruments is first-class — a ratio, a correlation, a spread between two symbols is ordinary arithmetic. Scanning the entire market, and managing many simultaneous positions, are not v1 concerns.

A bounded screener is a sealed pillar of its own, rolling out after v1: a total function mapped over a fixed-capacity universe, with a keyed top-K. That is a bounded map-reduce, never an unbounded scan — the totality rule reaches all the way up to the screener.

No solver, no invariant proofs

Kind bounds are presentation claims, not runtime guarantees. rsi really does have kind osc(0,100), and that bound tells the engine how to draw it — its own pane, a 0–100 scale, midline guides. But it does not enforce that the value stays in range at runtime. Only clamp makes a bound real:

FLUX
x = rsi(close, 14)
plot x                           // osc(0,100) — a claim about how to present it
plot clamp(x, 0, 100)            // clamp makes the bound a runtime fact

So Flux will not prove a relational invariant like high ≥ low for you. That would need a constraint solver, and Flux deliberately has none. Refusing the solver is what keeps the kind system decidable, fast, and honest about what it checks — the lattice is finite by family, so its laws are verified by enumeration, not proved by open-ended machinery. A type system that promised to prove your invariants and then quietly gave up on the hard ones would be worse than one that says plainly what it does and does not check.

No automatic converter from other ecosystems

Flux does not ship a translator from other charting-script languages, and cannot honestly ship a faithful one. Its semantics exclude by design patterns those ecosystems permit — retroactive edits to already-closed history, unbounded loops, ambient I/O — so a whole class of programs has no faithful translation at all. A converter that silently produced something for them would be worse than none: it would look like a port and behave like a guess. The documentation teaches the equivalent Flux patterns directly, by hand, which is the honest version of the same help.

Not a backend, not a general-purpose language

Flux is client-side and specialized. Despite having a server plane, it is not for writing a REST API, a database, an auth service, or a message queue. The server plane re-runs the same deterministic application to verify, persist and arbitrate — anti-cheat by re-execution, storage, authority — not a general backend runtime. For a backend, use a backend.

Nor is it a general application or systems language: no OS, no compiler, no 3-D modeller, no arbitrary CRUD-over-any-database business app. It excels at deterministic reactive dataflow with inferred presentation — indicators, charts, live dashboards, reactive UIs, verified games — and it is honestly not general-purpose. Specialized over a broad domain is the exact claim; not a toy, and not everything.

Determinism forecloses a few things too, and they are worth naming. Byte-identity forbids SIMD and floating-point reassociation, so heavy signal-processing or ML-inference workloads that lean on them are slower or out of scope. The pure planes have no true ambient randomness or wall-clock — both must be seeded or host-provided — so a true-RNG Monte-Carlo is modelled, not native. And the speed that Flux does deliver is honest about its source: the win is the datapath, not the engine. Shared sub-expressions compute once, dead code never computes, element-wise chains fuse into a single pass, and the moving parts of a scene cost zero JavaScript per frame. No “near-native,” no held frame rate — the aggregate work is compile-time budgeted and counted ([ErrSceneBudget]), and over-budget pressure degrades deterministically rather than being pinned to 60 FPS.

That determinism is the payoff for every limit on this page. The same program computes the same numbers and produces the same scene on every machine — the same bytes on ARM and x86, in the editor and in the shipped module, on the client and on the verifying server. Not the same pixels across different GPUs; the scene’s graphics and text both render on a WebGPU path — text as SDF glyphs, crisp at any zoom — and a GPU is free to rasterize the last pixel its own way. The promise is the numbers and the scene, held to the bit — and that promise is exactly what a total, sandboxed, I/O-free, solver-free language is able to keep.

See also

The formal rules →