Skip to main content

flow

Since v0.1.0 (initial language) · Top-level declaration

Grammar

flow <Name>(<param>: <Type>, <param>: <Type>, ...) -> <ReturnType> {
<FlowStep>
<FlowStep>
...
}

# FlowStep ::= step | if | for | let | return | weave | use | remember
# | recall | hibernate | associate | aggregate | explore
# | ingest | navigate | drill | corroborate | listen
# | retrieve | persist | mutate | run | reason | ...

A flow is the unit of orchestration in AXON. It declares a typed function whose body is an ordered sequence of cognitive operations — steps, control flow (if, for), bindings (let), memory operations (remember, recall), graph operations (navigate, drill), data-plane operations (retrieve, persist, mutate), and others.

A flow is what you run. The run statement binds a flow to a persona, a context, and any anchors that must hold across its execution.

Surface

flow is a top-level declaration. It is not nested inside another flow, a daemon, or any other primitive. (Flows compose by calling one another via apply from within a step, not by nesting.)

flow AnalyzeContract(doc: Document) -> ContractAnalysis {
step Extract {
given: doc
ask: "Extract parties, obligations, dates, penalties"
output: EntityMap
}
step Assess {
given: Extract.output
ask: "Identify ambiguous or risky clauses"
output: RiskAnalysis
}
return Assess.output
}

flow <Name>(<params>) -> <ReturnType>

  • <Name> — a PascalCase identifier, unique among flows within the same module. The compiler builds a per-module flow symbol table at parse time; duplicates are rejected.
  • (<params>) — zero or more comma-separated name: Type parameters. Types must resolve to a declared type, a built-in (String, Number, Bool), a generic application (List<T>, Stream<T>, Optional<T>?), or a recursive generic (FlowEnvelope<List<TenantRecord>>, since v2.0.0).
  • -> <ReturnType> — optional. When present, the flow's last evaluated step (or explicit return) must produce a value assignable to <ReturnType>. When omitted, the flow is treated as effect-only (no return value).
flow GreetUser(name: String, locale: String) -> Greeting { … }
flow EmitMetrics() { … } # no return
flow Chat(prompt: String) -> Stream<Token> { … } # streaming return

Body

The body is a { }-braced ordered sequence of flow steps. Each step is one of (see axon://primitives/<step-kind> for the detail of each):

KindPurpose
stepA cognitive operation (the most common)
if, for, let, return, break, continueControl + binding
reason, probe, weave, streamReasoning sub-constructs
remember, recall, hibernate, associate, aggregateMemory ops
explore, ingest, navigate, drill, corroborateGraph + retrieval
listen, retrieve, persist, mutateData plane
applyInvoke another flow (composition)

The compiler enforces three structural rules across the body:

  1. Definite assignment — each step that references <OtherStep>.output must lexically follow that step.
  2. Flow-locality — references are resolved against the enclosing flow's lexical scope; apply-invoked sub-flows do not see the caller's step outputs.
  3. Return-type compatibility — when -> <T> is declared, every return statement (or the last expression-step) must produce a <T>.

Conditions: the pure expression engine (v2.26.0)

An if condition is a pure, total, statically-typed expression (axon://logic/total_expressions). The closed operator catalog is: arithmetic + - * / %, comparison == != < <= > >=, boolean and / or / not, the collection/string builtins .length / .count / .is_empty / .is_null / .contains(x) / .starts_with(s) / .ends_with(s), and field/index access .field / [i]. Precedence is or < and < comparison < + - < * / % < unary < atom; parentheses group.

So an elementary check is native — no use Tool, no LLM step:

flow Throttle(recent: List<Call>, limit: Int) -> String {
if recent.length >= limit {
return "throttled"
}
return "ok"
}

The compiler type-checks the condition (axon-T810/T811/T812 for a type-incoherent operator, axon-T813/T814 for a builtin misuse) and const-folds it: a condition that is a constant (if 2 + 2 == 4) statically decides its branch and raises axon-W008 (the other branch is dead code). A reference of unknown static type is permissive. Evaluation is deterministic and side-effect-free — overflow and division-by-zero fail closed (the branch is not taken). A simple if x == "v" (a reference compared to a literal) keeps its pre-v2.26.0 form unchanged.

Streaming returns (v1.24.0)

A flow whose return type is Stream<T> participates in the algebraic stream effect runtime (v1.24.0). Each step that emits chunks does so via the <stream: ...> effect row; the runtime materialises the emissions as Server-Sent Events on the declared transport (axonendpoint route, socket, etc.).

flow Chat(prompt: String) -> Stream<Token> {
step Generate {
given: prompt
ask: "Reply, one token at a time"
output: Stream<Token>
}
}

The compile-time gate (flow_has_stream_output) checks that the final step's output type begins with Stream< and ends with >; adopters who only need batch returns can ignore the entire stream-effect surface.

Running a flow

A declared flow does nothing until a run statement binds it:

run AnalyzeContract(myContract)
as LegalExpert # persona binding (required)
within LegalReview # context binding (optional)
constrained_by [NoHallucination, NoPHI] # anchor list (optional)
on_failure: retry(backoff: exponential) # failure policy (optional)
output_to: "report.json" # destination (optional)
effort: high # compute hint (optional)

See axon://primitives/run for the full surface.

What this primitive is NOT

  • Not a function in the imperative sense. A flow is a typed orchestration of cognitive operations. The runtime is free to reorder data-independent steps and to fuse step bodies for backend efficiency, as long as observed effects respect the declared ordering and the IR audit trail.
  • Not nested inside another flow. Composition is via apply (which calls another declared flow) — there is no anonymous or inline flow grammar.
  • Not a daemon. A flow runs once per run; a daemon runs continuously and reacts to events. A daemon's bodies are expressed as flow-step bodies, but the lifecycle is different.
  • Not an axonendpoint. An axonendpoint exposes a flow over HTTP; the flow is the cognition, the endpoint is the wire.

See also

  • axon://primitives/step — the most common flow-step kind.
  • axon://primitives/run — the binding statement that executes a flow with a persona + context + anchors.
  • axon://primitives/axonendpoint — exposes a flow as an HTTP route.
  • axon://primitives/socket — exposes a flow as a session-typed WebSocket.
  • axon://logic/flow_composition — when to nest sub-flows via apply vs. inline steps.
  • axon://grammar/top_level — full top-level vs. nested table.