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:
plot ema(close, 20) // ✓ a constant window
// ✗ plot ema(close, len) — [ErrTotal]: a window bound must be a compile-time constantA 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:
update(m, Save) = { model: m, cmds: [ Persist("doc", m.doc) ] } // inert data — the host writes itPersist(…) 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:
subs(m) = [ OnSeries("funding-rate", GotFunding) ] // arrives as a message, never as a plotTo 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:
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 factSo 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
- Non-goals — the normative catalogue of everything on this page.
- Why Flux is built this way — the seven guarantees each limit protects.
- Determinism, replay and trust — what byte-identity buys, and its price.
- The four planes — the firewall that keeps external data out of ANALYSIS.
- Guarantees — each promise, and how it is machine-verified.
- FAQ — the “why does Flux not…?” answers, in short form.
The formal rules →
- The non-goals, catalogued — every limit above, stated once as the normative list: Non-goals.
- Totality — why
[ErrTotal]holds and what bounded iteration looks like: windows and bounded iteration.- Bounds are claims, not proofs — the finite lattice and where
clampmakes a bound real: Kinds — the dimensional type system.- Effects as inert data — the default-deny capability model behind host-mediated I/O: Capabilities — the security model and the capability catalogue.
- The reserved seams — how external data reaches ANALYSIS, and the
metrickind held open for it: net — the seam into analysis and compute — reserved seams.- Determinism — bit-identical compute, and what forecloses SIMD: the pinned-routine discipline.
- The server plane — re-run to verify, not a general backend: server.