# Changelog (/docs/changelog)
All notable changes to Fluxon are recorded here, newest first. The format follows
[Keep a Changelog](https://keepachangelog.com); the language follows
[Semantic Versioning](https://semver.org) — breaking changes to the language only
arrive with a version bump.
***
## v0.1.0 — June 2026 [#v010--june-2026]
The first tagged release. The language is feature-complete against the
[Agent Spec](/docs/reference/agent-spec): every battery in the spec is implemented,
and the Phase 0 stability bugs are all closed.
### Added [#added]
* **Language core** — indentation blocks, `=`/`<-` bindings, `fn`/lambda functions,
`if`/`elif`/`else`, the single `each` loop, `match`, and the `|>` pipe.
* **Error model** — `!` (propagate), `??` (nil-coalesce), `fail` (raise, with an
optional HTTP status), and `try`/`catch` (recover and continue).
* **Batteries** — `http` (server + client), `db` (SQLite, transactions, `tbl`
schema with declarative migration), `ai` (`ai.ask`/`ai.json`/`ai.run` with
automatic provider detection and OpenAI-compatible overrides), `ws`, `cron`,
`queue`, `reg`, `json`, `env`, `log`, plus the core `list`/`map`/`str`/`math`/
`rand`/`time` methods.
* **Tooling** — `assert` + `fluxon test`, an interactive REPL, the `par` parallel
fan-out primitive, leveled `log` output (`$LOG_LEVEL` / `$LOG_FORMAT`), and the
`.pkg` module manifest format.
### Changed [#changed]
* **`ai` default model** is `claude-opus-4-8` when `ANTHROPIC_API_KEY` is set.
* **`db` connection** is read from `$DATABASE_URL` automatically (default
`sqlite:fluxon.db`) — no connection code.
### Security [#security]
* `rand` is backed by the OS cryptographic CSPRNG (no predictable tokens).
* `Authorization` headers are dropped on cross-origin redirects.
* Request body size is limited by default (`max_body`, 10 MiB) — a DoS guard.
See the [Roadmap](/docs/roadmap) for the path ahead: a never-panic guarantee,
Postgres support, `fluxon fmt`, and an interactive WASM playground.
# Examples (/docs/examples)
## A complete small program (all together) [#a-complete-small-program-all-together]
```fx
use http db ai json
tbl notes
id serial pk
text str
ts now
http.on :post "/notes" \req ->
note = db.ins "notes" {text:req.body.text}
rep 201 note
http.on :get "/notes" \req ->
rep 200 (db.q "select * from notes order by ts desc")
log "server on :8080"
http.serve 8080
```
Here is the whole language. `use` it, declare a table with `tbl`, a route with
`http.on`, storage with `db` — no installs, no connection code, no boilerplate.
# Introduction (/docs/introduction)
> **What is Fluxon?** Fluxon is a programming language designed for backend systems
> that AI agents write well. Its philosophy: *"The language adapts to the AI, not
> the AI to the language."* There is **one** clear way to do each thing, the
> syntax uses few tokens, and the things you need most (HTTP server, database,
> AI/LLM calls, cron, queues) are built **into** the language — with no package
> installs.
Fluxon files are saved with the `.fx` extension.
This site is the complete, detailed **human** guide. If you want to teach Fluxon to
an AI agent, point it at the [Agent Spec](/docs/reference/agent-spec) — or just hand
it any page URL: agents and `curl` automatically receive raw Markdown instead of
HTML (see [For AI Agents](/docs/reference/for-agents)).
## Core ideas (read these first) [#core-ideas-read-these-first]
The 5 principles that set Fluxon apart from other languages:
1. **One task = one way (canonical form).** In other languages you can write the
same thing 5 ways (`while`, `for`, `do-while`...). In Fluxon there is **only
`each`** for iteration. There is **only one** way to print to the screen. The
reason for this rule: the AI does not think "which method should I choose?"
each time — there is no choice, so there are fewer mistakes.
2. **Few tokens, but readable.** The syntax is as short as possible, but *not
cryptic*. Keywords are spelled out in full (`each`, `match`, `else`) — because
a human or AI seeing Fluxon for the first time must understand them immediately.
3. **Batteries included (everything built in).** `http`, `db`, `ai`, `json`,
`cron`, `queue` — all of these are in the standard library. No `npm install`,
no `composer require`. You just say `use http` and use it.
4. **AI is a first-class primitive.** In other languages, calling an LLM means
installing an SDK, configuring a key, and parsing JSON. In Fluxon, `ai.json`
turns text into structured data in a single line and returns a confidence
score.
5. **Significant whitespace (indentation).** Blocks are separated not by `{}`
braces but by **indentation (2 spaces)** — just like Python. This removes
redundant characters.
## A taste of Fluxon [#a-taste-of-fluxon]
A complete HTTP + database app — no installs, no connection code, no boilerplate:
```fx
use http db ai json
tbl notes
id serial pk
text str
ts now
http.on :post "/notes" \req ->
note = db.ins "notes" {text:req.body.text}
rep 201 note
http.on :get "/notes" \req ->
rep 200 (db.q "select * from notes order by ts desc")
log "server on :8080"
http.serve 8080
```
## Where to go next [#where-to-go-next]
# Roadmap (/docs/roadmap)
479 green tests in the runtime, all batteries in the spec implemented, and the
Phase 0 stability bugs all closed. The current focus is **Phase 1** (hardening the
core), with **Phase 5** (distribution, beta) work starting in parallel.
The logic is simple: Phases 0–1 make the language *reliable*, Phase 2 *keeps* that
reliability *automatically*, Phase 3 makes it *useful*, Phase 4 *fast*, Phase 5
*publicly available*. The phases can run partly in parallel, but you don't enter
3–5 before 0–1 are done — you can't build an ecosystem on top of panics in the
foundation.
## Phase 0 — Stability: closing open bugs *(done)* [#phase-0--stability-closing-open-bugs-done]
The crash/DoS, security, and silent-incorrectness bugs from the full code review
have all been fixed and shipped with regression tests. Open issues with the `bug`
label are now **0**.
What was closed, by wave:
* **Wave 1 — crash/DoS:** `json.dec` panic on malformed JSON, no request body size
limit, unbounded recursion stack overflow, integer overflow panic,
`extract_from_table` Unicode panic, no client/`ai` timeout.
* **Wave 2 — security:** non-cryptographic `rand` for tokens, `Authorization` leak
on cross-origin redirect, dirty connection returned to the pool without ROLLBACK
on tx error.
* **Wave 3 — silent incorrectness:** `uniq(a, b)` dropping the multi-column
constraint, `ai` keeping only the last `tool_use` block, parser/lexer silent
errors `!x` / `m.0.1` / `1..n+1`, `db.up` empty-where malformed SQL, lost
repeated headers, queue handler-less busy-loop and shutdown job loss,
query-string percent-decoding.
Since the review, several language features also landed: `try`/`catch`, `assert` +
`fluxon test`, an interactive REPL, the `par` parallel fan-out primitive, and
leveled `log` output.
## Phase 1 — Hardening the language core [#phase-1--hardening-the-language-core]
What separates a real language from a toy is a definite answer to any input:
* **A guarantee never to panic.** Every panic path in the runtime turns into a
Fluxon-level error (`err`). To verify, the lexer / parser / `json.dec` are fuzzed
with `cargo-fuzz`.
* **Diagnostic quality.** Every error shows line:column + a code snippet + "did you
maybe mean this". This matters especially for AI agents — the more precise the
error message, the faster the agent fixes itself.
* **Stack trace.** A runtime error shows the Fluxon-level call chain.
* **Spec ↔ runtime audit.** Is there a test for every sentence in the agent spec?
When a discrepancy is found, either the spec or the runtime is fixed.
* Close the language gaps found in earlier real-project tests: `str` library gaps,
dynamic indexing, time arithmetic.
## Phase 2 — Reliability infrastructure [#phase-2--reliability-infrastructure]
* **Continuous fuzzing in CI** (nightly job): lexer, parser, json, http request
parsing.
* **Expand the `.fx` e2e suite** — "bad day" scenarios for each battery: network
drop, DB lock, large payload.
* **Benchmark suite + regression alert** — a basis for the later move to a VM.
* **Dogfooding harness.** Give an AI agent (with a cheap model) real backend tasks
and have it write them in Fluxon — every release. This method has found the most
real bugs so far.
## Phase 3 — Production-ready backend language [#phase-3--production-ready-backend-language]
* **Postgres** real support (currently an `Err` stub) — required for the "backend
language" claim. Fluxon `db.*` code is backend-neutral, the user code doesn't
change.
* **Deploy story:** single binary, graceful shutdown, `$PORT`/secrets convention.
Structured, leveled logging already landed.
* **`fluxon check`** — fast feedback for the AI agent loop. The CLI ships
`fluxon check ` today, but it is **lex + parse only**. A real
static/semantic check (unbound names, arity, type-shape) is still to do.
* **`fluxon fmt`** — canonical form is the language's philosophy, so a formatter is
mandatory. Still to do.
* **Module ecosystem:** `use ./file` exists; for now a firm "batteries-included is
enough" stance.
## Phase 4 — Performance [#phase-4--performance]
* Move from the tree-walking interpreter to a **bytecode VM** — but only after the
Phase 2 benchmarks show "where it's slow".
* On the HTTP path, full async or a thread pool instead of a thread per request.
## Phase 5 — Distribution and v0.1 *(beta starting)* [#phase-5--distribution-and-v01-beta-starting]
* **Install / packaging:** `curl | sh` + binaries on GitHub Releases, then
`crates.io` (`cargo install fluxon`), a Homebrew tap, Snap, and a PPA.
* **Documentation site + interactive playground** (compiled to WASM it runs in the
browser too).
* **English translation** — mostly done.
* **Versioning the spec:** the agent spec is frozen as v0.1, breaking changes only
with a version bump. A "real language" means a promise that code written today
still works tomorrow.
* **Editor tooling:** syntax highlighting (VS Code extension), then an LSP.
# ai — LLM (first-class primitive) (/docs/batteries/ai)
This is the biggest thing that sets Fluxon apart from other languages. The LLM is a
keyword, not an SDK. **The provider is detected automatically** (OS env or `.env`)
— you configure nothing:
* if `ANTHROPIC_API_KEY` is set → Claude (default `claude-opus-4-8`)
* if `OPENAI_API_KEY` is set → GPT (default `gpt-4o`)
* if both are set, Anthropic wins. Override: `$AI_PROVIDER` (`anthropic|openai`),
`$AI_KEY` (a shared key), `$AI_MODEL` (the model name).
This adapts to common standard names like `OPENAI_API_KEY`/`ANTHROPIC_API_KEY` —
the same `.env` works with other tools.
```fx
use ai
# Simple question-and-answer → text
answer = ai.ask "Translate this message into English: ${text}"
# Structured extraction (typed extraction) → a map according to the schema
schema = {
intent: ":new_order|:question|:other"
items: [{product:str qty:int}]
}
r = ai.json "Extract the order: ${text}" schema
# r.intent, r.items[0].product ...
```
## Audit metadata — automatic [#audit-metadata--automatic]
Each `ai.*` result carries metadata under `_`:
```fx
r = ai.json prompt schema
log r._.conf # confidence score (0..1)
log r._.tokens # tokens used
log r._.cost # cost
log r._.ms # latency (milliseconds)
```
This is the basis for confidence routing:
```fx
if r._.conf > 0.85
auto_answer r # high confidence → automatic
elif r._.conf >= 0.6
ask_owner r # medium → ask for confirmation
else
escalate_to_owner r # low → full escalation
```
`_.conf` is the calibrated confidence returned by the LLM battery. In real life
this should be backed by logprobs or self-eval; the language hides this behind the
battery.
## `ai.run` — agent tool loop (ONE step) [#airun--agent-tool-loop-one-step]
If the AI wants to use a tool, `ai.run` does **not** execute it itself — it returns
to you *what it wants to do*. You run the tool (with logging, cost, confirmation)
and return the result. The loop is **manual** — this gives you full control:
```fx
msgs <- [{role::user content:text}]
each i in 1..10 # maximum 10 steps
r = ai.run msgs tools # tools: a list of [{name desc params}]
if r.kind == :final
ret r.text # AI is done → final answer
# r.kind == :call → the AI wants to call tools. The model may call several
# in parallel → all are in r.calls; return a result for EACH one.
each c in r.calls
out = reg.call c.tool c.args # run the tool by name (see below)
log "tool ${c.tool}" # logging/cost/confirmation goes here
msgs <- msgs.push {role::tool id:c.id content:(json.enc out)}
```
`r.tool`/`r.args`/`r.id` mirror `r.calls[0]` for back-compat (single-tool code
still works). But on parallel calls you must return a result for every
`tool_use_id`, otherwise the next request 400s. `ai.run` is intentionally
single-step. If you let the AI's tool calls run automatically and uncontrolled,
you could not do logging/cost/confirmation. The loop is yours — so you see and
control every tool call.
See [`reg`](/docs/batteries/reg) for the registry that turns a tool name into a
callable function.
## Connecting to other providers (advanced) [#connecting-to-other-providers-advanced]
Everything above works with **zero config** — a standard key in `.env` is all you
need, and most programs never go further. But the `ai` battery also speaks any
**OpenAI-compatible** API (Z.AI / GLM, OpenRouter, Ollama, vLLM, LM Studio, Azure,
Groq, …). You override the parts of the request that differ, and **nothing else
changes** — the default behavior is byte-for-byte the same when you pass no
overrides.
There are two ways to override, and they compose (per-call wins over global).
`ai.config {…}` — set global defaults once, at the top of the program (like
`http.cors`; call it before `http.serve`). The keys, all optional:
| key | meaning |
| --------- | --------------------------------------------------------------------------------------------------------------- |
| `url` | full endpoint URL (replaces the provider default) |
| `style` | wire format: `:openai` or `:anthropic` (the request/response shape) |
| `key` | API key (same role as `$AI_KEY`, inline) |
| `model` | model name (same role as `$AI_MODEL`, inline) |
| `headers` | extra HTTP headers, **merged** onto the defaults (hyphenated names like `HTTP-Referer` must be **string keys**) |
| `extra` | extra request-body fields, **merged** into the JSON |
A map key is a bare identifier, which cannot contain `-`. HTTP header names with a
hyphen must therefore be written as **string keys**: `{"HTTP-Referer": "…"
"X-Title": "…"}`. Header names are matched case-insensitively (and sent
lowercased), so a per-call header replaces a global one regardless of casing.
```fx
# GLM (Z.AI): the OpenAI wire format at a different URL — that's the whole change.
ai.config {
style: :openai
url: "https://api.z.ai/api/paas/v4/chat/completions"
key: env.ZAI_KEY
model: "glm-4.6"
}
answer = ai.ask "Salom, dunyo" # now goes to Z.AI
# OpenRouter: extra body params + its recommended headers.
ai.config {
style: :openai
url: "https://openrouter.ai/api/v1/chat/completions"
key: env.OPENROUTER_KEY
model: "anthropic/claude-3.5-sonnet"
headers: {"HTTP-Referer": "https://myapp.dev" "X-Title": "My App"} # hyphenated → string keys
extra: {provider: {sort: "throughput"}} # OpenRouter-specific knob
}
```
A trailing **opts map** on any `ai.ask`/`ai.json`/`ai.run` overrides the global
config for that one call (same keys). Omit it and the call behaves exactly as
before — it is purely additive:
```fx
# Same global config, but this one call uses a cheaper/faster model.
r = ai.ask "quick check" {model: "glm-4.5-air"}
r = ai.json prompt schema {model: "glm-4.6"}
r = ai.run msgs tools {extra: {temperature: 0}}
# To pass opts to ai.run WITHOUT tools, use nil for tools:
r = ai.run msgs nil {model: "glm-4.6"}
```
`headers` and `extra` **merge** key-by-key (a key you give wins; the rest of the
defaults stay), while `url`/`style`/`key`/`model` **replace**. `style` selects only
the wire format — a GLM endpoint speaks OpenAI, so `style::openai` + a custom `url`
is enough; you don't need the official OpenAI key. Env equivalents exist for the
scalars: `$AI_STYLE`, `$AI_BASE_URL` (alongside `$AI_KEY` / `$AI_MODEL` /
`$AI_PROVIDER`).
When you point at a custom host, you must supply the key inline (`key:` / opts) or
via `$AI_KEY`. A standard provider key (`$OPENAI_API_KEY` / `$ANTHROPIC_API_KEY`)
is **never** sent to a custom URL — so an OpenAI key in your environment can't leak
to Z.AI/OpenRouter. Custom URL with no explicit key is a clear error before any
network call.
# core — list, map, str, math, rand, time (/docs/batteries/core)
All of these are **core** — they work without `use` (just like `log`).
## `list` — list methods [#list--list-methods]
On a value, `.method`:
```fx
l.len # length
l.push x # adds an element → a new list
l.filter \x -> x > 0 # keeps those matching the condition → a new list
l.map \x -> x * 2 # transforms each → a new list
l.has x # is it inside → bool
l.index x # index of the first matching element, -1 if not found
l.find \x -> x > 4 # first element matching the predicate, nil if not found
l.0 l.1 # element by index
l.slice a b # the a..b range (b excluded) → a new list
l.join ", " # → text: [1 2 3].join "," → "1,2,3"
l.reduce 0 \acc x -> acc + x # accumulate: (initial value, function)
l.sort # natural order (numbers or strings) → a new list
l.sort \a b -> a.p - b.p # comparator returns a number: negative → a first
l.reverse # reversed order → a new list
l.uniq # removes duplicates (first occurrence kept)
l.flat # flattens one level: [[1 2] [3]] → [1 2 3]
l.zip other # pairs up: [1 2].zip ["a" "b"] → [[1 "a"] [2 "b"]]
l.any \x -> x > 4 # does any match → bool (stops at first match)
l.all \x -> x > 0 # do all match → bool (stops at first mismatch)
```
To build a list use `l.push x`, **not** `l + [x]`. To filter, use `l.filter`
instead of a manual `each` loop; to build text, use `l.join` instead of a manual
accumulator:
```fx
# Manual (long): With methods (clean):
result <- [] result = items.filter \t -> t.active
each t in items
if t.active
result <- result.push t
text <- "" text = names.join ", "
each n in names
text <- text + n + ", "
```
## `map` — key-value methods [#map--key-value-methods]
On a value, `.method`:
```fx
m.set k v # sets/updates a key → a new map
m.del k # removes a key → a new map
m.merge other # merges two maps (other's keys win) → a new map
m.has k # is the key present → bool
m.keys # a list of keys
m.vals # a list of values
m.key m[k] # read (m[k] — dynamic, variable key)
```
To **write** to a map use `m.set k v`. `m[k]` only **reads** (does not write).
This is consistent with lists: `push` for a list, `set` for a map. Shared state
(for example, who is in which room in realtime) is managed with these methods.
## `str` — text functions [#str--text-functions]
```fx
str.len s # length (number)
str.slice s 0 3 # the 0..3 range (3 excluded): "hello" → "hel"
str.up s # UPPERCASE
str.low s # lowercase
str.split s "," # split → a list: "a,b" → ["a" "b"]
str.has s "part" # is it inside → bool
str.int "42" # text → number
str.str 42 # number → text
str.trim " s " # strips leading/trailing whitespace → "s"
str.replace s "-" "+" # replaces every "-" with "+"
str.starts s "/api" # starts with prefix → bool
str.ends s ".fx" # ends with suffix → bool
str.pad "7" 3 "0" # pads on the LEFT → "007"
str.repeat "ab" 3 # repeat → "ababab"
```
List length is a member (`list.len`), text length is a module function
(`str.len s`). The reason: a list and text are separate types, and their
operations should not mix. If both were the same `.len` it would be confusing.
## `math` — math [#math--math]
```fx
math.floor 3.7 # → 3
math.ceil 3.2 # → 4
math.abs -5 # → 5
math.min 3 7 # → 3 (ints in, int out)
math.max 3 7 # → 7
math.pow 2 10 # → 1024 (int ^ non-negative int → int)
math.sqrt 9 # → 3.0 (always flt; negative input is an error)
```
## `rand` — random [#rand--random]
```fx
rand.int 1 100 # a random integer in the range 1..100
rand.str 6 # a random string of 6 characters (short codes)
```
`rand` is backed by the OS cryptographic CSPRNG, so its output is not predictable.
But **length matters too**: `rand.str 6` yields only \~36 bits of entropy (62⁶) —
fine for a short code, but brute-forceable as a secret. For session IDs, tokens,
and other secrets use at least `rand.str 24` (\~140+ bits).
## `time` — time and date [#time--time-and-date]
```fx
time.now # the current time (timestamp)
time.ago 24 :hr # the time 24 units ago. Units: :sec :min :hr :day
time.in 60 :min # the time 60 units later (TTL/expiry). Same units
time.fmt t "..." # format a timestamp into text
time.sleep 1 # waits 1 second (flt too — 0.5). Polling/retry backoff
time.parse "2026-06-10T10:00:00Z" # arbitrary ISO text -> canonical UTC timestamp ("Z"/"±HH:MM")
time.add t 30 :min # offset from ANY timestamp (not now): end_at = start_at + duration
time.sub t 5 :min # mirror of time.add — shift backward (e.g. buffer before)
time.diff a b # (a - b) difference in seconds (int); / 60 -> minutes
```
Difference between `time.in`/`time.ago` (offset from **now**) and
`time.add`/`time.sub` (offset from **any** given timestamp): a booking server
computes `end_at = time.add start_at 30 :min` from a client-supplied `start_at`.
Instead of writing raw `now() - interval '24 hours'` in a DB query, use `time.ago`
— it is clean and safe:
```fx
r = db.one "select count(*) c from tickets where created > $1" [time.ago 24 :hr]
```
**Duration & interval recipes** (interval arithmetic IS available —
`time.add`/`diff` exist):
```fx
end_at = time.add start_at dur :min # duration: start + dur minutes
mins = (time.diff end_at start_at) / 60 # gap between two times -> minutes
overlap = a.start < b.end & a.end > b.start # do two intervals overlap? (bool)
buf_start = time.sub start_at 15 :min # buffer: 15 min before start
```
**IANA timezone / DST** — `time.parse` takes an optional zone name; `time.fmt`
takes an optional zone as a third argument. Wall-clock ↔ UTC conversion is DST-aware
(NOT a fixed offset), so "09:00 local every day" lands on the correct UTC instant
across summer/winter transitions:
```fx
utc = time.parse "2026-07-15 09:00:00" "Asia/Tashkent" # local wall-clock -> UTC
loc = time.fmt utc "HH:mm" "America/New_York" # UTC instant -> zone wall-clock
```
A wall-clock time in a spring-forward gap (e.g. `02:30` on the night clocks jump)
does not exist and raises an error; an unknown zone name raises too.
# cron — scheduling (/docs/batteries/cron)
A standard **Unix 5-field** cron expression: `minute hour day month weekday`.
Every AI agent knows this format (crontab, GitHub Actions, ...). `cron.on` reads
the expression **without quotes** — here `*` is not multiplication, it is a cron
marker:
```fx
use cron
cron.on 0 * * * * check_prices # at the start of every hour (minute=0)
cron.on 30 9 * * * daily_check # every day at 09:30
cron.on 0 18 * * 0 briefing # Sunday (0) at 18:00
cron.on */15 * * * * poll # every 15 minutes
cron.on 0 9 * * 1-5 \-> # weekdays at 09:00 (inline lambda)
log "weekday"
```
Fields: `*` any value, `*/N` every N, `A-B` a range, `A,B,C` a list. Weekday:
0=Sunday ... 6=Saturday.
`cron.on` **does not block** — like `http.on` it just registers, and the scheduler
runs in the background. A server (`http.serve`/`ws.serve`) keeps the process alive,
and cron runs in the background at its scheduled times. Order: `cron.on` calls go
**before** `http.serve`.
For a cron-only script (no server) — `cron.run` takes over the process:
```fx
cron.on 0 9 * * * daily_check
cron.run # blocks: the program does not end, cron keeps running
```
Convenience: you can also write the expression with quotes (`cron.on "0 9 * * *" f`)
— the result is the same. For an AI the canonical form is without quotes (fewer
tokens).
# db — database (SQLite) (/docs/batteries/db)
The connection is **automatic**: it is read from the `$DATABASE_URL` environment
variable (default `sqlite:fluxon.db`). You write no connection code.
Today the only working backend is **SQLite** (bundled via `rusqlite` — no server
to run). Set `DATABASE_URL=sqlite:app.db`, `sqlite::memory:`, or `sqlite:file:...`.
**Postgres and MySQL are planned, not yet wired** — a `postgres://`/`mysql://` URL
currently errors out. The `db` API is written to be backend-agnostic, so your code
will not change when they land.
```fx
use db
# Query — the result is a list of maps
rows = db.q "select * from products where owner=$1" [owner_id]
# A single row (or nil)
user = db.one "select * from users where id=$1" [id]
# Insert — returns the inserted row
row = db.ins "orders" {cust:5 total:0 status::new}
# Update — db.up "table" {changes} {condition}
db.up "orders" {total:1500} {id:order_id}
# Delete — db.del "table" {condition}
db.del "cart_items" {id:item_id}
# UPSERT — db.put "table" {changes} {key}
# updates if it exists by key, inserts if not (atomic)
db.put "agent_memory" {val:v} {agent:aid key:k}
```
For the "update if it exists, insert if not" pattern (memory, cache, counters).
If you did this by hand with `db.one` + `if` + `db.ins`, two parallel requests
might both see "not there" and insert twice (a race). `db.put` makes it atomic.
## Transactions — `db.tx` [#transactions--dbtx]
If a multi-step mutation must be **atomic** (for example checkout: order + line
items + decrementing stock), wrap it in a `db.tx` block. If an error (`fail` or
`!`) occurs inside the block, **all** changes are **rolled back** — the DB never
stays in a half-finished state:
```fx
db.tx \->
ord = db.ins "orders" {cust:c.id total:total}
each it in items
db.ins "order_items" {ord:ord.id prod:it.id qty:it.qty price:it.price}
db.up "products" {stock:it.stock - it.qty} {id:it.id}
db.up "carts" {status::converted} {id:cart.id}
# if it reaches the end of the block — commit. If a fail happens midway — all cancelled.
```
`db.tx` can also return a value (via `ret`):
```fx
ord = db.tx \->
o = db.ins "orders" {...}
ret o # the block value goes outside
```
### Concurrency (parallel requests) guarantee [#concurrency-parallel-requests-guarantee]
`db.tx` takes the write lock up front (`BEGIN IMMEDIATE`) and a contending
transaction waits (up to the `busy_timeout`) instead of racing. This means the
"read → check → modify" pattern is safe. For example, two parallel withdrawals
from one account — both cannot go through at once, and there is no overdraft:
```fx
db.tx \->
acc = db.one "select * from accounts where id=$1" [aid]
if acc.balance < amt
fail 422 "insufficient balance"
db.up "accounts" {balance:acc.balance - amt} {id:aid} # race-safe
```
In other languages you would write `SELECT FOR UPDATE`, locks, or mutexes for
this. In Fluxon it is not needed — `db.tx` guarantees it itself. "The language
adapts to the AI": the AI does not think about locks, it just writes inside `db.tx`.
### Idempotency — not performing the same operation twice [#idempotency--not-performing-the-same-operation-twice]
In places like money transfers, a client may resend a request. Protect it with a
unique key (a `uniq` column): first check whether it exists, then write the key
inside a transaction — if it is a duplicate, the `uniq` error → tx rollback:
```fx
old = db.one "select * from transactions where ikey=$1" [key]
old ?? (ret old) # already done → return the old result
db.tx \->
db.ins "transactions" {ikey:key amount:amt ...} # duplicate → uniq → rollback
# ... transfer the money
```
This is **mandatory** for places like e-commerce checkout. Without a transaction,
if an error happens midway, you can end up with some stock decremented but no
order created.
* Parameters via `$1, $2...`, values passed as a list `[...]`.
* In `db.ins`/`db.up`, the map keys are column names.
* **A query without parameters** does not need a list: `db.q "select * from links"`.
* An **aggregate (count/sum)** can return `nil` on an empty table — protect it
with `?? 0`:
```fx
r = db.one "select count(*) c, sum(clicks) s from links"
log "links: ${r.c}, clicks: ${r.s ?? 0}"
```
## Schema declaration — `tbl` [#schema-declaration--tbl]
You declare tables in Fluxon itself:
```fx
tbl products
id serial pk
owner int ref:users.id
name str
price money
status sym index|uniq # multiple modifiers on one column → pipe `|`
ts now
index(owner status) # multi-column index (space-separated, no commas)
uniq(owner price) # multi-column unique
```
Type keywords: `serial int flt str bool json now sym money`. Modifiers: `pk`
(primary key), `uniq`, `index`, `null`, `ref:table.column` (foreign key).
### Indexes and uniqueness [#indexes-and-uniqueness]
For a single column, append a word modifier: `index`, `uniq`. To put **both** on
one column the canonical form is `|` (pipe): `status sym index|uniq`. The spaced
form (`index uniq`) is also accepted. For **multi-column**, use a separate
parenthesized line: `index(a b)`, `uniq(a b)` — space-separated by default (no
commas, to save tokens); a comma is also accepted: `index(a, b)`. **Index names
are automatic** (`idx__` / `uniq_<...>`) — you never invent a name. A
name that is too long (DB limit is 63 bytes) is automatically shortened (with a
deterministic hash suffix); your code never breaks.
### Declarative migration — `tbl` is the single source of truth [#declarative-migration--tbl-is-the-single-source-of-truth]
You only write the latest shape of the `tbl`; Fluxon diffs it against the current
DB and runs the necessary DDL **itself**:
* new column → `ADD COLUMN`;
* column removed from `tbl` → `DROP COLUMN` (the table is first backed up to
`_fluxon_bak_*`);
* a `tbl` removed entirely → `DROP TABLE` (with backup; **only Fluxon-managed**
tables — a manually created table is never touched);
* index added/removed → `CREATE/DROP INDEX`.
Migration is **idempotent** — re-deploying the same `tbl` is safe, nothing breaks.
No migration SQL needed for schema changes. Type changes and renames are **not**
automatic — do those manually with `db.q "ALTER TABLE ..."`, and Fluxon syncs the
rest afterward.
### Special column types [#special-column-types]
**A `json` column** — when read it **automatically becomes a map/list** (not a
string, no need for `json.dec`); when written, a map/list is automatically encoded.
**The `money` type — for money.** Money should NEVER be a `flt` (float) — float
rounding errors corrupt money. `money` is a whole number of **minor units**
(tiyin, cents): `15000` = 150.00 so'm. All money math uses `money`/`int` (`int`
is 64-bit):
```fx
tbl accounts
id serial pk
balance money # in tiyin, e.g. 15000 = 150.00
total = price * qty # int math, not float
```
**The `sym` type — for enums.** If a column is `sym`: the DB stores **text**, but
when Fluxon reads it, it automatically returns a **symbol**. On writing and
filtering, a symbol is automatically converted to text. Then `match` works directly:
```fx
tbl tickets
category sym # DB: text ("billing"), Fluxon: symbol (:billing)
status sym
# Writing: you give a symbol, the DB stores text
db.ins "tickets" {category::billing status::new}
# Reading: if the schema says sym, Fluxon returns a symbol
t = db.one "select * from tickets where id=$1" [id]
match t.category # t.category is a symbol, so match works
:billing -> log "billing matter"
:technical -> log "technical"
_ -> log "other"
# Filtering: a symbol is passed, automatically converted to text
db.q "select * from tickets where category=$1" [:billing]
```
**One rule:** a `sym` column — text in the DB, a symbol in Fluxon, conversion
automatic.
# env — environment variables (/docs/batteries/env)
```fx
port = env.PORT ?? "8080" # directly env.NAME
key = env.AI_KEY
```
# http — server & client (/docs/batteries/http)
## Server [#server]
You declare a route on a single line:
```fx
use http
http.on :post "/notes" \req -> rep 201 {ok:true}
http.on :get "/notes/:id" \req -> rep 200 {id:req.params.id}
http.serve 8080
```
* `http.on :method "/path" handler` — a route. The method is a symbol (`:get`,
`:post`, `:put`, `:patch`, `:del`).
* The handler is a lambda. Its argument is `req`:
* `req.body` — the JSON body (automatically converted to a map)
* `req.params.id` — the `:id` in the path
* `req.query` — query parameters (`?key=val`)
* `req.headers` — headers
* `rep status body` — the response. If `body` is a map, it **automatically**
becomes JSON.
* `http.serve port` — starts the server.
* `http.serve port {max_body: BYTES}` — configures the request body size limit
(DoS guard). Default `10 MiB` (10485760 bytes); over the limit the server
returns `413 Payload Too Large` without buffering the body. `max_body: 0`
disables the limit (unlimited — only behind a trusted internal network).
## File upload (`multipart/form-data`) [#file-upload-multipartform-data]
Files sent by a browser form or `curl -F` land in `req.files`, plain form fields
in `req.body` (symmetric with JSON):
```fx
http.on :post "/upload" \req ->
f = req.files.0
fs.write f.filename f.content
rep 201 {saved:f.filename size:f.size}
```
* Each file: `{name filename content size}`. `content` is a str for UTF-8 text,
bytes for binary (image, PDF); `size` is always the **byte** count.
* `req.files` is always a list — empty when the request is not multipart
(`each` works without a nil check).
* The `max_body` limit applies to multipart bodies too.
## Redirect [#redirect]
There is no special verb — with `rep` you give a 302 status and a `location` key;
it becomes the Location header:
```fx
http.on :get "/:code" \req ->
link = db.one "select * from links where code=$1" [req.params.code]
link ?? (rep 404 {error:"not found"})
rep 302 {location:link.url}
```
## Route precedence [#route-precedence]
If two routes overlap (`/:code` and `/stats/:code`), the **literal (exact) path
automatically wins** — regardless of the order written. `/stats/:code` is always
checked before `/:code`.
## Client [#client]
Calling an external API:
```fx
res = http.get "https://api.example.com/data"
res = http.post url {key:"val"} # the body becomes JSON automatically
# res.status, res.body, res.headers (a map, keys lowercased)
loc = res.headers.location # or res.headers["content-type"]
```
A redirect (3xx) is **not followed by default** — `res.status` is 30x, and
`res.headers.location` is read. If you need automatic following, add an options
map:
```fx
res = http.get url {follow:true} # 3xx → follows Location
res = http.get url {follow:true max:5} # hop limit (default 10)
# res.hops — how many redirects happened
```
Exceeding `max` is an error. The options map is the last argument:
`http.post url body {follow:true}`.
## Custom request headers [#custom-request-headers]
For APIs that require authentication (`x-api-key`, `Authorization`,
`anthropic-version`...), add `headers` to the options map — this is symmetric with
`res.headers` in the response:
```fx
res = http.post "https://api.anthropic.com/v1/messages" body {
headers: {
"x-api-key": env.ANTHROPIC_API_KEY
"anthropic-version": "2023-06-01"
}
}
```
If a header value is not a string it is converted to text; a header with a `nil`
value is dropped. If the user provides `content-type`, that is used instead of the
automatic `application/json`.
# json (/docs/batteries/json)
```fx
use json
s = json.enc value # value → JSON text
v = json.dec str # JSON text → value
```
# log — leveled logging (/docs/batteries/log)
```fx
log "message" # = log.info, to stderr for diagnostics
log.debug "detail"
log.info "msg"
log.warn "careful"
log.err "failed"
```
Levels are ordered: `debug < info < warn < err`. Bare `log` = `log.info` (old code
keeps working). Control the noise in production via env:
* `$LOG_LEVEL` (debug/info/warn/err) — minimum level; anything **below** it is
silenced. Unset → everything prints.
* `$LOG_FORMAT=json` — each line a JSON object (`{time, level, msg}`) for log
aggregators (Loki/ELK). Otherwise a human-readable `[LEVEL] message`.
```bash
LOG_LEVEL=warn ./app # only warn and err
LOG_FORMAT=json ./app # {"time":"...","level":"info","msg":"..."}
```
# queue — background queue (/docs/batteries/queue)
So a webhook can respond quickly, you offload heavy work to the background:
```fx
use queue
queue.on "send" \job -> tools.send job.ph job.body # the handler
queue.push "send" {ph:phone body:text} # add to the queue
```
* `queue.on ` — the handler for jobs with this name. The handler
takes a single `job` argument — this is the payload given to `queue.push` (a map).
* `queue.push ` — adds a job to the queue. The payload is optional
(if not given, `nil`). It **does not block** — it returns immediately, and the
job runs in the background.
* Jobs run **on a single worker thread, FIFO (in arrival order)** — ordering is
guaranteed. An error inside a handler does not kill the worker.
* If `push` is written before `on`, the job **waits in the queue** and runs once
the handler is registered (order-independent).
* The worker is a background thread — it processes the queue while a server
(`http.serve`/`ws.serve`) or `cron.run` holds the process. In a queue-only script
you need one of these blocking calls to hold the process.
# reg — function registry (/docs/batteries/reg)
Storing and calling a function **by its string name**. Essential for agent tools:
the AI gives you the tool **name** (text), and you must turn it into a function and
call it.
```fx
reg.add "calc" \args -> args.a + args.b # name → function
reg.add "search" \args -> http.get "/s?q=${args.q}"
out = reg.call "calc" {a:2 b:3} # call by name → 5
reg.has "search" # is it in the registry → bool
reg.names # a list of all names
```
Otherwise, you would have to execute the tool name coming from the AI with
`match name` (a hardcoded switch) — changing the code for each new tool. With
`reg`, tools are added **at runtime** (`reg.add`), and the AI calls any of them
with `reg.call`. You simply cannot build an agent platform without this.
See [`ai.run`](/docs/batteries/ai#airun--agent-tool-loop-one-step) for the loop
that produces the tool name and arguments.
# ws — websocket (realtime) (/docs/batteries/ws)
For real-time applications (chat, live updates). Where `http` is request-response,
`ws` is a persistent two-way connection.
```fx
use ws
ws.on :connect \conn -> # a new connection. conn.id — a stable unique id
ws.data.set conn :user nil # ws.data — session state for THIS connection
ws.on :message \conn msg -> # msg — the incoming text (if JSON, json.dec it)
m = json.dec msg
ws.send conn (json.enc {ok:true}) # reply to THIS connection
ws.on :disconnect \conn ->
ws.room.leave conn "ch:5"
ws.serve 9000
```
* `ws.on :event handler` — events: `:connect`, `:message`, `:disconnect`. The
`:message` handler is `\conn msg ->` (msg — the incoming **text**), the others
are `\conn ->`.
* `ws.send conn text` — sends to THIS connection (text; if you need JSON,
`json.enc`).
* `ws.data.set conn :key value` / `ws.data.get conn :key` — session state for THIS
connection (Fluxon keeps it until the connection drops, and clears it on
disconnect).
* `ws.serve port` — starts the server (blocking).
## Rooms — for broadcast [#rooms--for-broadcast]
Sending to a group at once. Fluxon manages rooms itself — you do not maintain a
manual "who is in which room" map:
```fx
ws.room.join conn "ch:5" # add the connection to a room
ws.room.leave conn "ch:5" # remove it from the room
ws.room.send "ch:5" (json.enc {t:"msg" body:b}) # send to EVERYONE in the room
ws.room.members "ch:5" # the room members (for presence)
```
`http.serve` and `ws.serve` work **together** (on different ports). Room membership
and presence are managed inside `ws.room` — no manual shared-state map is needed.
# Control flow (/docs/guide/control-flow)
## Conditions: `if` / `elif` / `else` [#conditions-if--elif--else]
```fx
if x > 0
log "positive"
elif x == 0
log "zero"
else
log "negative"
```
Keywords are spelled out **in full** (`elif`, `else`) — so they are understandable
at a glance.
`if` also works as an **expression** (ternary equivalent): it returns a value on
one line. The `else` branch is required. Wrap calls in the condition in parens.
```fx
pad = if h < 10 ("0" + str.str h) else (str.str h) # leading-zero
kind = if n % 2 == 0 "even" else "odd" # simple choice
r = if (str.len s) > 0 "full" else "empty" # call condition → parens
```
## Iteration: `each` (the only loop) [#iteration-each-the-only-loop]
Fluxon has **only one** loop — `each`. It iterates over a list, range, or map.
There is **no** `while`, `for`, or `do-while`:
```fx
each item in list # list elements
log item
each i in 1..5 # range: 1,2,3,4,5
log i
each k, v in map # map: key and value
log "$k = $v"
```
Inside a loop:
* `skip` — move to the next iteration (in other languages `continue`)
* `stop` — exit the loop (in other languages `break`)
```fx
each n in nums
if n < 0
skip # skip negatives
if n > 100
stop # stop if over 100
log n
```
If you need to repeat based on a condition: iterate over a range
(`each i in 1..n`) or use recursion. One loop — one way.
## Selecting by value: `match` [#selecting-by-value-match]
Comparing one value against several variants. Mostly for symbols:
```fx
match status
:new -> log "new"
:confirmed -> log "confirmed"
:cancelled -> log "cancelled"
_ -> log "unknown" # _ = default
```
`match` and `if` do **different things**: `if` is for a boolean condition,
`match` is for distributing one value across variants. That is why both exist.
`match` only works with a **value** (symbol or number). For a boolean condition
(like `conf > 0.85`) **always use `if/elif/else`**. Writing `match true` and
putting conditions under it is **wrong** — do not do this:
```fx
# WRONG:
match true
conf > 0.85 -> ...
# CORRECT:
if conf > 0.85
...
```
# Error handling (/docs/guide/errors)
In Fluxon a function can return success (`ok`) or an error (`err`). The **one**
primary way to work with errors is the `!` operator, and `??` for `nil`.
## `!` — automatically propagate the error upward [#--automatically-propagate-the-error-upward]
If you put `!` after a function name: if it returns an error, the error is
**automatically** propagated to the caller (you do not check it by hand). If it
succeeds, you get the result:
```fx
fn process id
user = db.one "select * from users where id=$1" [id]!
# if db.one returns an error, process also returns that error —
# the next line never runs
log user.name
```
This shrinks the multi-line `if err != nil { return err }` pattern into **a
single character**.
## `??` — an alternative if nil [#--an-alternative-if-nil]
If a value is `nil` (not an error, just empty), provide an alternative with `??`:
```fx
name = user.name ?? "guest"
each it in items
p = db.one "...price..." [it.product]
p ?? (ask_owner "Price?"; skip) # if p is nil — ask and skip
log p.price
```
## `fail` — raise an error [#fail--raise-an-error]
Raise an error from your own code:
```fx
if qty < 1
fail "invalid quantity"
```
**`fail` with a status code — for expected errors.** If you give `fail` a status
code inside an HTTP handler, it **automatically** turns into a response with that
status. This replaces `try/catch`: for an expected error, instead of deep nesting
just `fail`:
```fx
http.on :post "/transfer" \req ->
acc = db.one "select * from accounts where id=$1" [req.body.from]
if acc.balance < req.body.amount
fail 422 "insufficient balance" # → 422 {error:"insufficient balance"} to the client
# ... the main path, no nesting
```
* `fail 4xx "message"` — an **expected** (business) error → a JSON response with
that status.
* `fail "message"` (no status) — an **unexpected** error → 500.
## `try` / `catch` — catch an error [#try--catch--catch-an-error]
Often propagating the error (`!`) or turning it into an HTTP response
(`fail 4xx`) is enough. But sometimes you must **catch the error and keep going** —
give a default when an external API is down, retry, or log the error and continue
the request. That is what `try`/`catch` is for:
```fx
user = try
api.get "https://..."! # an error here?
catch e
log "api down: ${e.message}" # e = {message, status}
cached_user # → value of the catch body
```
* `catch e` — `e` binds to a `{message, status}` map. `status` is the `fail`
status code, or `nil` for a status-less `fail` or a runtime error.
* `catch` (no variable) — ignores the error.
* Like `if`, `try`/`catch` is an **expression**: it yields the `try` body value
on success, the `catch` body value on error.
* `ret`/`skip`/`stop` are control flow, **not errors**: they pass through `try`
uncaught.
* Re-raise from inside `catch` with `fail`.
`!` = propagate the error, `??` = replace nil, `fail` = raise an error (with or
without a status), `try`/`catch` = catch and continue. For expected request errors
prefer `fail 4xx` (the code stays flat); reach for `try`/`catch` only when you must
recover and continue.
# Functions (/docs/guide/functions)
A function is declared with `fn`. Arguments are separated by **spaces** (no
commas):
```fx
fn add a b
ret a + b
```
## Single-line function [#single-line-function]
If the body is a single expression, you can write it on one line with `->`:
```fx
fn double x -> x * 2
```
## Return [#return]
Two ways, but they give the same result:
* `ret x` — explicit return
* **The last expression** is returned automatically (without `ret`)
```fx
fn add a b
a + b # the last expression — returned automatically
fn check x
if x > 0
ret "positive" # ret is needed for an early return
"non-positive" # the last expression
```
`ret` is used only when you need an **early** (mid-function) return. At the end —
just write the expression.
**`ret` also works inside a lambda.** This matters most in HTTP handlers. Instead
of a deep `if/elif/else` pyramid for validation, write a **guard clause** (early
exit) — the code stays flat:
```fx
# ❌ Deep nesting (bad): ✅ Guard clause (good):
http.on :post "/x" \req -> http.on :post "/x" \req ->
if req.body.email if !req.body.email
if req.body.body ret rep 400 {error:"email required"}
rep 201 (...) if !req.body.body
else ret rep 400 {error:"body required"}
rep 400 {...} rep 201 (db.ins "t" {...})
else
rep 400 {...}
```
## Calling a function [#calling-a-function]
Arguments are separated by spaces, without parentheses:
```fx
add 2 3 # → 5
double 4 # → 8
```
Parentheses are only needed for **grouping** (passing the result of one function
into another):
```fx
double (add 2 3) # first add 2 3 = 5, then double 5 = 10
```
**A no-argument function is called with empty parentheses `()`.** Since a call
without parentheses is defined by its arguments, this is the only way to call a
function that has no parameters. This clearly distinguishes a name (value) from a
call:
```fx
fn new_id -> rand.str 8
new_id() # CALL → a new random id each time
new_id # NOT a call → the function VALUE (for callback/reg)
```
`f(x)` (argument inside parentheses) **does not work** — the canonical form is
`f x`. Empty `()` is only for a no-argument call (one task = one way).
## Lambda (anonymous function) [#lambda-anonymous-function]
With the `\` character, used inline:
```fx
\x -> x * 2
each_map nums \x -> x * 2 # multiply each element by 2
```
# Lexical basics (/docs/guide/lexical)
## Comments [#comments]
There is only one kind of comment — from a `#` character to the end of the line:
```fx
# This is a comment
x = 5 # This is also a comment
```
Fluxon has **no** `//` or `/* */`. One way — `#`.
## Statements [#statements]
Each statement ends at a **new line**. A semicolon (`;`) is **not needed** and is
not used:
```fx
x = 5
y = 10
```
## Blocks [#blocks]
A block is opened not with `{}` but with **indentation**. Each level is **2
spaces**. The block ends when the indentation decreases:
```fx
if x > 0
log "positive"
log "this line is also inside the block"
log "outside the block"
```
# Modules (import / export) (/docs/guide/modules)
## `use` — import a module [#use--import-a-module]
You import the standard library or your own file. There is no installation
(`install`):
```fx
use http db ai json # standard batteries — multiple modules with spaces
use ./tools # your own file → tools.function
```
After importing, names live under the module: `db.one`, `http.serve`,
`tools.create_order`.
### `as` — renaming (alias) [#as--renaming-alias]
If your own file has the same name as a battery (for example an `ai.fx` file and
the `ai` battery), there is a clash. Rename your own module with `as`:
```fx
use ai # the battery
use ./ai as helper # your own file → helper.classify (no clash)
```
Do not give your own files battery names (`ai db http cron`...), or if you do,
rename them with `as`.
## `exp` — export [#exp--export]
Expose a function or value from your file to other files:
```fx
exp fn create_order items customer
...
exp price_limit = 1000
```
Only things marked with `exp` are visible from the outside.
See also: the [`.pkg` manifest format](/docs/reference/pkg-format) for shipping a
reusable module with an AI-readable doc.
# Operators (/docs/guide/operators)
## Arithmetic [#arithmetic]
```fx
+ - * / % # add, subtract, multiply, divide, remainder
```
**`+` also concatenates strings.** If the operands are numbers it adds; if they
are text it joins:
```fx
1 + 2 # → 3
"hel" + "lo" # → "hello"
```
The type itself decides the difference — one operator, two natural behaviors.
## Comparison [#comparison]
```fx
== != < <= > >=
```
## Logical [#logical]
```fx
& # and
| # or
! # not — before a value: !x
```
## Special operators [#special-operators]
### `??` — null-coalesce [#--null-coalesce]
If the left side is `nil`, it gives the right side:
```fx
port = env.PORT ?? "8080" # if PORT is missing, "8080"
name = user.name ?? "guest"
```
### `.` — member access / index [#--member-access--index]
Map key, list index, length:
```fx
user.name # map key
list.0 # first element of the list
list.len # length
m[key] # dynamic key (via a variable)
list[i] # computed index (via an expression)
list.(i) # computed index through `.` — same as list[i]
```
### `..` — range [#--range]
Both ends are inclusive:
```fx
1..5 # [1 2 3 4 5]
```
### `|>` — pipe [#--pipe]
Passes a value into a function, removing nested notation:
```fx
result = data |> clean |> format
# this is equivalent to: format(clean(data))
```
# Values & types (/docs/guide/values-types)
Fluxon has the following basic types:
| Notation | Type | Description |
| ---------------- | ------- | ---------------------------------------------- |
| `42` | `int` | Integer |
| `3.14` | `flt` | Fractional number (float) |
| `"hello"` | `str` | Text (string) |
| `true` / `false` | `bool` | Boolean value |
| `nil` | `nil` | "Nothing" / emptiness |
| `[1 2 3]` | `list` | List — elements separated by **spaces** |
| `{a:1 b:2}` | `map` | Key-value pairs — separated by **spaces** |
| `:ok` | `sym` | Symbol — for enums/tags |
| — | `bytes` | Binary data — no literal, comes from functions |
## Important subtleties [#important-subtleties]
### Binary data (`bytes`) [#binary-data-bytes]
For non-text data like images, PDFs, archives. There is no literal syntax —
values come from functions: `fs.readb path` (binary file read), `crypto.b64db s`
(binary-safe base64 decode), `bytes.of s` (text → its UTF-8 bytes). Core operations:
```fx
b = fs.readb "logo.png" # bytes (nil if the file is missing)
bytes.len b # BYTE count (str.len counts CHARS)
bytes.str b # bytes → text (explicit error if not UTF-8)
bytes.slice b 0 4 # sub-bytes
fs.write "copy.png" b # fs.write/append accept str or bytes
rep 200 b {content_type:"image/png"} # raw binary HTTP response
```
In logs/interpolation bytes render as `` — raw bytes never leak into
text. `crypto.sha256`/`b64`/`hex` inputs take str or bytes.
### No commas in lists and maps [#no-commas-in-lists-and-maps]
Elements are separated by spaces. This is intentional — commas waste tokens:
```fx
nums = [1 2 3 4]
user = {name:"Aziza" age:30 active:true}
```
### Putting a variable inside text (interpolation) [#putting-a-variable-inside-text-interpolation]
With `"${...}"` you embed an expression inside text:
```fx
name = "Aziza"
log "Hello ${name}!" # → Hello Aziza!
log "Total: ${price * qty} so'm" # an expression also works
```
For a simple variable you can shorten it to `"$name"`, but for an expression
`${...}` is required.
### Multi-line text (block strings) [#multi-line-text-block-strings]
For long prompts, SQL, or templates use `"""`. Content starts on the next line,
and the common indentation of the lines is stripped automatically — so the block
sits naturally inside indented code:
```fx
prompt = """
You are a helpful agent.
User question: ${question}
"""
```
If the closing `"""` is on its own line, the text has no trailing `\n`.
Interpolation and `\n`/`\t` escapes work as in normal strings; `"` can be
written freely without escaping (handy for JSON/HTML fragments).
### Symbols — instead of enums [#symbols--instead-of-enums]
To represent states, use a symbol instead of text. `:new`, `:confirmed` — these
are cheaper in tokens and clearer than the text `"new"`:
```fx
status = :confirmed
dir = :in
```
When a symbol is converted to text (interpolation, `str.str`, `+`, `log`) the
`:` prefix is dropped — the value is `florist`, the `:` is a syntax marker:
`str.str :florist` → `"florist"`, `"path/${:florist}"` → `"path/florist"`.
Inside a list/map, the `:` is kept (`[:a]` → `[:a]`), because there a symbol
needs to stand out from text.
### Truthiness [#truthiness]
`nil` and `false` are falsy. Everything else (including `0`, `""`, and the empty
list) is **truthy**. This simple rule is intentional: only two things are falsy.
# Variables (bindings) (/docs/guide/variables)
Fluxon has **two** kinds of binding, and they do **different things** (which is why
having two does not violate the canonical rule). The model is Python's: an
assignment is **local to the current function**, and there is no immutability —
any name can be re-bound.
## `=` — bind a local (the default) [#--bind-a-local-the-default]
```fx
x = 10
name = "Aziza"
x = 20 # re-binding is fine — = just updates x
```
`=` binds in the **current function**. `if`/`each`/`match` blocks are
transparent (they open no new scope), so the accumulator pattern reads naturally:
```fx
total = 0
each n in [10 20 30]
total = total + n # updates the same `total`
# total == 60
```
Inside a function, `=` always makes a **local** — it never touches an outer or
global variable of the same name (shadowing, like Python).
## `<-` — reassign, reaching out of the function [#---reassign-reaching-out-of-the-function]
Use `<-` to write to a variable that lives in an **enclosing** function or at the
top level — it crosses the function boundary (closure capture):
```fx
counter <- 0
inc = \n ->
counter <- counter + n # writes the OUTER counter, not a local
inc 5 # counter == 5
```
Use `=` for a normal (local) value. Reach for `<-` only when a function must
write to a variable defined *outside* it — that is the one thing `=` will not do.
When you see `<-`, you know "this reaches out and changes something shared".
# Agent Spec (/docs/reference/agent-spec)
The **Agent Spec** is the single authoritative description of Fluxon, written for
machine consumption: dense, canonical, and token-minimal (\~2700 tokens). It is the
file an AI agent reads once to learn how to write correct Fluxon code. Where this
site's guide is narrative, the Agent Spec is pure reference.
The spec is the **source of truth** for the language's syntax. It is versioned with
the language: breaking changes only arrive with a version bump (see the
[Changelog](/docs/changelog)).
## Get the spec [#get-the-spec]
## For an agent [#for-an-agent]
Point your agent at the raw file directly:
```bash
curl https://docs.fluxon-lang.com/fluxon-agent.md
```
That single file is enough for an agent to start writing Fluxon. For the full,
detailed treatment of any feature, every page on this site is also available as raw
Markdown — see [For AI Agents](/docs/reference/for-agents).
# For AI Agents (/docs/reference/for-agents)
These docs are built for AI agents as much as for humans. **Any page is available
as raw Markdown**, and agents get it automatically — no query parameter, no special
knowledge required.
## Just use the page URL [#just-use-the-page-url]
When a human copies a docs URL and hands it to an agent, the agent fetches it with
`curl` (or a fetch tool). The server detects a non-browser client and returns **raw
Markdown** instead of HTML:
```bash
curl https://docs.fluxon-lang.com/docs/batteries/http
# → returns the raw Markdown of the http battery page
```
A browser visiting the same URL gets the full styled HTML page. Same URL, two
representations — chosen by the request, not by you.
## Or ask for Markdown explicitly [#or-ask-for-markdown-explicitly]
Three equivalent ways to force Markdown:
```bash
# 1. Append .md to any docs path
curl https://docs.fluxon-lang.com/docs/batteries/http.md
# 2. Send an Accept header
curl -H "Accept: text/markdown" https://docs.fluxon-lang.com/docs/batteries/http
```
## Whole-site bundles [#whole-site-bundles]
For loading the entire documentation into a context window:
* **[`/llms.txt`](/llms.txt)** — a structured index of every page (titles, URLs,
descriptions). Start here to discover what exists.
* **[`/llms-full.txt`](/llms-full.txt)** — every page's full Markdown concatenated
into one document. Drop the whole language into your context in a single request.
## The compact spec [#the-compact-spec]
If you want the single, canonical, token-minimal description of the language — the
file an agent reads to learn Fluxon — use the **[Agent Spec](/docs/reference/agent-spec)**.
It is the authoritative reference, organized for machine consumption rather than
narrative reading.
# .pkg manifest format (/docs/reference/pkg-format)
A **battery-shaped module** is a reusable Fluxon module (`use ./lib/s3`) that ships
an optional sibling `.pkg` manifest. The manifest's mandatory `doc` block is the
micro-equivalent of a built-in battery's entry in the agent spec: a short,
canonical doc the AI agent reads **instead of** the implementation code.
This solves a *reuse + knowledge-packaging* problem: write the code once, and let
the agent understand and use it correctly and cheaply. It is deliberately **not** a
package manager — there is no transitive resolution and no version ranges.
## Location [#location]
The manifest lives next to the module file, with the `.pkg` extension:
```
lib/s3/s3.fx → lib/s3/s3.pkg # package-style layout
lib/foo.fx → lib/foo.pkg # single-file module
```
## Format [#format]
Line-oriented, with exactly two required keys. Lines starting with `#` are
comments; blank lines are ignored. **Unknown keys are an error** (so typos like
`nam` or `doc:` surface instead of being silently dropped).
```
name s3
doc """
### s3 (object storage upload)
WHAT: upload a file to S3/R2 + presigned URL.
CANONICAL:
use ./lib/s3
url = s3.upload "bucket" "key.png" bytes {content_type:"image/png"}!
GOTCHAS:
- content_type is required, else the browser downloads instead of rendering.
- never put `../` in the key.
DEPENDS: crypto http # AWS Signature V4
"""
```
* **`name`** — the package name (one value after the keyword).
* **`doc`** — a triple-quoted block string. The opening `"""` must be alone on its
line (content starts on the next line); the closing `"""` is alone on its own
line. The block is **dedented** (the smallest common leading indentation is
stripped), so you can indent the doc to taste.
### Doc conventions [#doc-conventions]
`WHAT` / `CANONICAL` / `GOTCHAS` / `DEPENDS` are **conventions inside the free-text
doc**, not parsed keys — mirror a battery's entry in the agent spec:
* **WHAT** — one line: what the module does.
* **CANONICAL** — the one canonical way to use it. Reference exported names as
`name.fn` (e.g. `s3.upload`). The runtime cross-checks these against the module's
actual `exp`-orts.
* **GOTCHAS** — the non-obvious correctness traps.
* **DEPENDS** — which **built-in batteries** the module uses (`crypto http`). A
package may depend ONLY on built-in batteries — never on another package. This
flat graph is what keeps Fluxon out of the npm/pip dependency-hell trap.
## Validation (load-time, soft) [#validation-load-time-soft]
When a module is loaded with `use ./...`, the runtime looks for a sibling `.pkg`.
The policy is intentionally lenient so existing modules keep working:
| Situation | Result |
| --------------------------------------------------------------------------- | ---------------------------------------- |
| No `.pkg` sibling | Module loads (backward compatible) |
| `.pkg` present, valid doc | Module loads |
| `.pkg` present, **empty doc** | **Load fails** — the AI-doc is mandatory |
| `.pkg` present, **malformed** (e.g. unterminated `doc` block) | **Load fails** |
| `.pkg` present but **unreadable** (invalid UTF-8, a directory, permissions) | **Load fails** |
| `CANONICAL` references a name not `exp`-orted | **Warning** on stderr, still loads |
Only a genuine *file-not-found* is the backward-compatible no-manifest case; any
other read failure means a manifest is present but unusable and is surfaced. The
`CANONICAL` reference check resolves names against the manifest's own `name` field
(so a vendored `aws.fx` carrying `name s3` is checked against `s3.`).
The empty-doc and malformed cases are hard errors because a manifest that exists on
purpose but carries no usable doc defeats the entire point. The missing-`exp` case
is only a warning: the doc may legitimately mention a not-yet-implemented form, and
breaking the load over a doc typo would be too aggressive.
## How this differs from npm/pip [#how-this-differs-from-npmpip]
| | npm/pip | Fluxon `.pkg` modules |
| -------------------- | ------------------------- | ----------------------------------------- |
| What the agent reads | code / long README | mandatory short canonical doc |
| Transitive deps | A→B→C→D hell | only batteries; package→package forbidden |
| Versioning | `^1.2` (nondeterministic) | exact pin / local vendoring |
| Quality | scattered | doc mandatory — no doc = invalid |
| Philosophy | many ways | `one task = one way` |