# 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` |