Skip to main content

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
  1. Parse. Text is parsed into the AST; a JSON AST is deserialized into the same structure. The two forms are interchangeable from here on.
  2. 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 (repeat needs a count, loop needs a deadline), nested suspend points (await and checkpoint are top-level only), and concurrency hazards (duplicate branch names, return inside a parallel branch, two branches binding the same symbol).
  3. 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.
  4. 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:

valuetruthy?
nullno
falseno
0no
"" (empty string)no
"false"no
"0"no
[] (empty array)no
{} (empty object)no
anything elseyes

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:

  • await suspends 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.
  • checkpoint is 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.