Execution model
This page explains what happens after flux receives an authored flow: parsing, analysis, optimization, execution, value storage, dispatch, errors, and suspension. Execution is deterministic except where a flow explicitly calls a model or waits for external input.
Lifecycle
parse (or receive JSON) -> analyze -> optimize -> execute
- Parse. Text is parsed into the AST; a JSON AST is deserialized into the same structure. The two forms are interchangeable from here on.
- Analyze. The analyzer lowers the flow to a typed form and rejects what it cannot reason
about: unknown operations, wrong arity, incompatible argument types, unbounded loops
(
repeatneeds a count,loopneeds a deadline), nested suspend points (awaitandcheckpointare top-level only), and concurrency hazards (duplicate branch names,returninside aparallelbranch, two branches binding the same symbol). - Optimize. The optimizer may parallelize or reuse provably safe read-only work. It never
changes what a plan is allowed to do — dispatch authorization remains the runtime floor.
The scheduler summarizes every top-level statement across its whole subtree (nested
blocks, conditions, templates, call arguments): a statement whose reachable operations are
all registered and read-only may run concurrently with other such statements when their
symbol reads and writes are independent. A statement containing a write/network/process
effect, an unknown operation (unknown effects are treated as the most dangerous
effects), an approval/durability construct (
confirm,await,checkpoint,once,saga,thing), or a cross-turn rate construct (throttle,debounce) is a hard fence: nothing is scheduled across it in either direction, so approval order and policy behavior are exactly those of sequential execution. The optimized run is observationally equivalent to the sequential one — same bound values, same user-visible trace order. - Execute. The interpreter runs the body top to bottom, dispatching every operation through the safety envelope and recording a run trace.
A flow that fails analysis never executes at all. That is the point: malformed flows are rejected before they can touch the world.
JSON AST tooling
Every node's programmatic form is a JSON object whose kind field names the node type. The strict
schema (fluxlang schema) spells out each variant for validators and tooling. The merged schema
(fluxlang schema --merged) is a compact union used by language-workbench experiments and external
hosts. Neither schema is part of the conversational model contract: the default agent calls live
operations through their provider-native schemas and never emits an AST.
Symbols and values
Symbols are names; values are immutable records in a value store the runtime owns.
- A bind (
x = op(…)) stores the operation's result and points the symbol at it. - Rebinding a symbol stores a new value — the old value is not mutated and stays addressable in the audit trail.
- An unbound symbol reference is a hard error at evaluation time.
Because there is no hidden mutable environment, a run is fully described by its value log and run trace. That is what makes flows auditable and replayable—you can always answer "what did this symbol hold when that step ran?"
Operation dispatch
call is the boundary between the language and the world. The language knows only the
operation's name and arguments; the host decides what operations exist and routes every
call through one chain:
authorization -> approval -> redaction -> guarded IO
No node kind bypasses it — not parallel branches, not retry bodies, not composite ops. The
catalog of operations a plan can target is the host's concern, not the language's: see
Operations.
Pure nodes never dispatch. fmt, jq, expr, parse, the obj/list value templates,
peek, and the context-pack nodes (ctx, ctx_append) perform no IO and never pause for
approval. Use them instead of shelling out for arithmetic, string formatting, or JSON
extraction — see Pure data.
String interpolation
Any string literal (and the fmt node) may embed {symbol} placeholders. Substitution happens
at evaluation time, from the symbols bound at that moment. A placeholder whose name is not
bound is left verbatim — no silent data loss. To emit a literal brace, double it: {{ produces
{, }} produces }.
Truthiness
Every condition position — when, unless, assert, await's optional when guard, and the
until guards of repeat and loop — uses the same JSON truthiness:
| value | truthy? |
|---|---|
null | no |
false | no |
0 | no |
"" (empty string) | no |
"false" | no |
"0" | no |
[] (empty array) | no |
{} (empty object) | no |
| anything else | yes |
A tool that returns the string "false" reads as falsey, so branching on a shell wrapper's or
boolean tool's textual output works as expected.
Conditions can be a symbol, literal, call, or native expression:
when $count > 3 && $state == "ready"
return "go"
repeat 10, until: len($queue) == 0
poll()
Native expression conditions lower to pure expr nodes; no tool is dispatched to evaluate them.
Errors
An errored call aborts the flow: nothing is bound, execution stops, and the error propagates —
unless an enclosing node handles it. try catches, retry re-attempts transient failures,
fallback moves to the next branch. Fatal errors (a policy denial, an unknown op, a type
error) are never retried. See Reliability & guard rails.
Inside parallel, a failing branch still merges the completed branches' output — a
deterministic prefix — before its error propagates. See Concurrency.
Suspend and resume
Two nodes make flows outlive a single execution:
awaitsuspends the flow at a top-level statement until an external event arrives. The runtime records the suspend point and persists the flow; when the awaited input arrives, the flow resumes at the next statement with the received value bound. The already-completed prefix is not re-executed.checkpointis a durable resume marker: re-running the same flow in the same session fast-forwards past the completed prefix — its symbols are still bound and its side effects are not repeated. The flow's declared name and canonical body identity form the resume key, so an edited flow does not inherit the old cursor; the checkpoint label is descriptive event metadata.
Both are top-level only so resume cursors stay simple and stable. The full
cross-turn story — including memo, once, scope, and saga — is in
Durability & cross-turn state.
What this buys you
- Reviewability. An authored flow and any proposed action batch are explicit values.
- Auditability. The value log and run trace record what happened, in order, with inputs.
- Replayability. Deterministic execution over immutable values means a stored flow can be re-run — and a suspended one resumed — without guessing at hidden state.
Related docs
- Tooling — commands that parse, preview, and execute flows.
- Types & effects — annotations and effect tags the analyzer reads.
- Durability & cross-turn state — nodes that suspend and resume execution.