Skip to content

lumen.toml reference

lumen.toml sits at the app root, beside the src/ directory holding the app's code, and declares everything static about the app: its entry file, window, skin, locale, script engine, runtime modules, build hooks, and subsystem settings. The file is optional; every key has a default.

Unknown top-level sections and unknown keys inside a section are rejected with a parse error naming the offending key. A parse error aborts the command.

Command line flags override the matching config key.

[app]
entry = "main.lmn"
locale = "de-DE"

[window]
title = "My App"
size = [1280, 720]

[skin]
name = "auto"

[script]
engine = "candela"

[app]

Key Type Default Effect
entry string "main.lmn" Markup entry filename, resolved under the app's src/ directory.
id string the app directory name Stable identifier for per-app state directories, including the one a script's files::data_dir() writes to, and the app id notifications are attributed to.
kind "markup", "rust", "cpp", "python" auto-detected Pins the build and run route instead of letting the directory contents decide.
locale BCP-47 tag the OS locale, else en-US The locale the app starts in. Selects which locale/<tag>.ftl catalogue translatable markup and the scripts' t() builtin resolve against; every catalogue in the directory is loaded regardless. It also picks the number, date, and currency formatter behind the format attribute and the scripts' format_* builtins, and sets the app's base writing direction, which a dir attribute overrides. A script changes the locale later with set_locale(tag); this names where the app starts. A tag that is not valid BCP-47 is a parse error.
fallback_locale BCP-47 tag en-US The language the app's source strings are written in. A key missing from the active locale's catalogue is looked up here before the element falls back to its own authored text. Set it to the active locale to leave nothing to fall through to. It is also the catalogue lumenc i18n extract writes by default. A tag that is not valid BCP-47 is a parse error.
single_instance bool false A second launch forwards its command-line arguments to the already-running window (on_second_instance(args)) and exits instead of opening a second one. Windowed runs only; a --headless run never locks. See OS integration.

Auto-detection for kind looks for a Cargo.toml depending on lumen (rust), a CMakeLists.txt (cpp), or a .py file importing lumen (python), and falls back to markup.

[window]

Key Type Default Effect
title string the app directory name Window title.
size [w, h] integers [960, 720] Window size in logical pixels. lumenc run --size overrides it.
remember_state bool false Persists window position, size, and maximised state on close and restores them on the next launch.

[pages]

Multi-page navigation. See Pages.

Key Type Default Effect
entry string see below Home page key: a filename stem with no .lmn. Ignored when no page has that key.
enabled bool on when src/ holds more than one .lmn file, or include names more than one page that exists Forces multi-page mode on or off.
include array of strings directory discovery Explicit ordered page-file list. When set, only these files are pages. Paths are relative to src/ and may point into a subdirectory. An entry whose file does not exist is skipped with a warning.

Without include, every .lmn file in src/ is a page except layout.lmn, which contributes its <template> declarations to every page instead of becoming one. Discovery does not descend into subdirectories, so a page kept in one needs an include entry naming it, plus enabled = true when src/ itself holds fewer than two .lmn files. A page's key is its filename stem either way: pages/settings.lmn is settings.

An include entry naming a file that is not there yet is skipped: lumenc prints a warning naming the path, the page is not navigable, and it does not count toward the multi-page default. So the block can list the pages an app will have before they are written. When every entry is missing there is no page set left to run, and the app fails to load naming the file the list asked for.

The entry key resolves in this order: [pages] entry when it names an existing page, then index, then the [app] entry stem, then main, then the first page alphabetically. A skipped entry is not an existing page, so an entry naming one falls through to the next candidate.

[skin]

Key Type Default Effect
name "default", "macos", "windows", "linux", "auto" none Applies an embedded platform skin beneath the app's own CSS. Equivalent to <root skin="...">. auto picks the skin matching the host OS.

With no skin named, no platform skin applies. A small user-agent stylesheet setting per-tag sizing floors applies to every app either way.

[script]

Key Type Default Effect
engine "candela", "rhai", "lua" per file Forces every script in the app onto one host. Matched case-insensitively; an unrecognised value falls back to candela.

With the key absent, each script file picks its host from its own extension: a .cdl file runs under candela, a .lua file under Lua, a .rhai file under Rhai. An app holding more than one language runs one host per language. An inline <script> block has no extension to read; it joins the app's one external language when there is exactly one, and candela otherwise. Set engine when that is not the host you want, most often for an inline script written in something other than candela.

[mcp]

The introspection and automation server. See Testing and Tooling.

Key Type Default Effect
port integer 7878 TCP port the server listens on, bound to 127.0.0.1. 0 disables the server outright.
simulate bool false Lets the server inject pointer, key, and scroll events. Required by lumenc click, type, key, and scroll.
issues bool false Lets lumen_framework_status shell out to git and gh to list the checkout's open GitHub issues.

The server runs by default for a windowed run. A headless run turns it off unless simulate = true or [runtime] mcp = true.

With simulate on, the server snapshots every tick instead of once a second, so an automation driver sees each frame.

The introspection port has no authentication, so issues defaults to off: turning it on lets any caller that can reach 127.0.0.1:<port> trigger a git/gh subprocess. With it off, lumen_framework_status reports that the lookup is disabled instead of running either command.

[profile]

Key Type Default Effect
mode "off", "chrome", "tracy", "stderr" "off" Installs the tracing profiler for lumenc run. --profile overrides it.

chrome writes lumen-trace.json in the current directory, tracy connects to a running tracy-profiler, and stderr prints per-system spans live. All three need a lumenc built with the profiling cargo feature; tracy also needs profiling-tracy.

[asset_roots]

Key Type Default Effect
paths array of strings empty Extra directories scanned for relative src= paths. Relative entries resolve against the app directory; absolute entries are used as given.

[perf]

Per-cache memory budgets.

Key Type Default Effect
images_mb integer 64 Decoded-image cache cap, in MB.
shape_entries integer 512 Text shape-cache cap, in entries.
scene_fragments integer 256 Scene-fragment cache cap, in entries.

[runtime]

Forces a subsystem on or off at startup instead of letting the runtime decide. Every key is optional; leaving one out keeps the automatic behaviour.

Key Type Default Effect
mcp bool on for a windowed run, off for a headless one Runs the introspection server. [mcp] port = 0 still disables it.
hot_reload bool on only for a windowed run from source Watches source files and reloads on change. Off for headless, bounded, and artifact runs.
threads integer min(cpu count, 4) Worker-thread budget. LUMEN_THREADS overrides this.

These gate startup, not linkage: the code is still in the binary. To leave a subsystem out of a build, use [capabilities].

[capabilities]

Which optional subsystems a build of this app carries, one key per capability name. Two builds read it: lumenc package --static, which links the app from a prebuilt kit and takes only the capabilities the app needs, and lumenc bundle --static, which compiles a runtime from source. The shared runtime and lumenc run always carry everything, and ignore this section.

An entry settles its capability outright. A capability the section leaves out follows its own rule: most are carried when the app's sources (markup, scripts, styles, this file) mention the builtins they answer, and the development ones only when asked for here. A key naming a capability the kit does not carry stops the package and lists what it does.

Key Carried by default when Effect
http-fetch the app calls fetch( or http( The HTTP client behind the scripts' fetch() and http() builtins. Without it both calls report the missing client.
async the app calls a file-dialog builtin The async bridge file dialogs resolve on; on macOS they do not open without it.
os-filedialog the app calls a file-dialog builtin The file dialogs.
os-hotkey the app calls register_hotkey Global hotkeys.
os-notify the app calls notify Notifications.
os-tray the app calls tray_icon The tray icon.
os-launcher the app calls open_url, open_path or reveal_path The URL and file launcher.
os-power the app calls keep_awake Sleep inhibit.
os-lifecycle the app uses recent files, autostart or single_instance Recent files, autostart, single-instance launch.
mcp never; mcp = true asks for it The introspection server.
devtools never; devtools = true asks for it The in-window devtools overlay.

bundle --static maps the names it knows (http-fetch, async, mcp) to the features it compiles with and ignores the rest.

The script host is selected by [script] engine or inferred from the app's script files, not here.

[web]

What lumenc web needs that only a site has. The rest of the file still describes the app: [window] title is the documents' title, [app] gives the entry file and the locale, [pages] the page set, [asset_roots] where an asset comes from, and [script] engine which engine runs the app's code.

[capabilities] does not apply. A site loads one prebuilt runtime that ships with the toolchain, so there is nothing per-app to compile or trim.

Key Type Default Effect
out_dir string dist/web Where the site is written, relative to the app directory unless absolute.
base_path string / URL prefix the site is served under. Every link and asset reference hangs off it.
url string none Absolute site URL. The canonical link, the social metadata, the hreflang links and the sitemap need an address; without url or canonical they are left out.
description string none Description any page without one of its own carries.
og_image string none Image for social previews, relative to the site root or absolute.
canonical string url Absolute URL the pages declare as canonical, for a site published at more than one address.
locales array of BCP-47 tags the app's locale Emit the site in each locale. render = "static" and render = "csr" write one document tree per locale; render = "ssr" writes none and a render answers in the locale the request asks for.
default_locale BCP-47 tag [app] locale, else en-US Which of the emitted locales is served from the site root; the others sit under /<tag>/. This picks a tree, not a fallback: what a missing key resolves to is [app] fallback_locale.
skin string [skin] name, else default Skin the site is styled with. auto is not read here: it means the machine's own OS, and a site is served to every OS.
css "sheet", "computed" sheet sheet emits the stylesheet the app was written with. computed writes the values Lumen's cascade resolved onto each element instead, which answers what Lumen resolved but loses states, media queries and anything created later.
widgets "semantic", "verbatim" semantic Which shape a widget the parser built out of smaller elements is emitted as. Today both emit the parts.
render "static", "csr", "ssr" csr Where a page's document comes from. csr writes it, along with the runtime, the compiled app and the manifest the pages load. static writes it and nothing to run it. ssr writes what a render needs and no documents: a page is produced for the request that asks, by lumenc web --serve or by a server built on lumen-ssr. Every value writes the whole markup tree, so a reader and a crawler get the same document.
runtime bool what render implies Whether the documents carry the browser runtime. render = "static" already means false and render = "csr" already means true, so saying the opposite alongside either is refused, naming the value that means it. render = "ssr" is the one that leaves the question open: with false, a page is produced for the request that asks and carries no wasm and no boot script, so it is read and never taken over.
prerender "seeds", "run", "none" seeds Where the state the pages are rendered with comes from. seeds uses [web.seed] and the defaults the markup declares; run starts from those and then runs the app during the build, writing each page with the state it settles into; none renders the markup alone. run with render = "ssr" is refused: a rendered page settles its own state per request.
hash_assets bool false Add a content hash to asset file names. Not applied yet.
debug_attrs bool false Write the extra data-lm-* attributes naming what an element came from. Not written yet.
menubar "omit", "nav" omit What an app menu bar becomes in a document.
sitemap bool on when the site has an address Write sitemap.xml: every page that asks to be indexed, in every locale, each entry carrying when its sources last changed and an hreflang link to the same page in the site's other languages. The URLs are built from canonical, else url; with neither, no file is written.
robots bool on when a sitemap is written Write robots.txt: it allows every crawler everything and names the sitemap when one was written. It carries no Disallow for a page with index = false, because a crawler that does not fetch a page never reads the tag that keeps it out of an index. Set it to write the file with no sitemap to name, or unset it to ship your own.
host "static", "netlify", "vercel", "apache", "nginx" static Where the site is deployed. A named host also gets the file that makes it serve a deep path with a 200 (_redirects, vercel.json, .htaccess, nginx.conf); static relies on the emitted 404.html, which every host serves. Under render = "ssr" no rewrite file is written, because a render answers a deep path itself.
navigation "soft", "hard" soft How a link to another page of the same site is followed. soft swaps the page in place with the app still running, puts that page's own URL in the address bar, and wires the browser's back and forward buttons to the site. hard lets the browser load each document, the same as an ordinary site. The key governs what a link does, not what a script does: a page() call swaps the page in place under either value, and only soft puts the page it lands on in the address bar.
[web]
out_dir   = "dist/web"
base_path = "/"
url  = "https://example.com"
host = "netlify"
locales = ["en-US", "de-DE"]

[web.seed]
count = 3
greeting = "Hello"

[[web.seed.todos]]
id    = "1"
title = "write it down"

[web.pages.settings]
title = "Settings"
description = "Everything you can change"
index = false

[web.seed] gives the signals the pages are rendered with, and the same values are handed to the runtime so it starts where the page left off. A value is a string, a number or a boolean. A name written as [[web.seed.<name>]] instead is an array signal: each entry is one row, its keys are the fields a <for> row template reads, and the values are strings. A <for each="<name>"> is emitted with those rows in it, and an element bound to a seeded signal with bind-text, bind-checked, bind-value or bind-disabled is emitted showing that value. A seeded signal beats the default the markup beside the binding would have set, and a script that publishes the signal itself beats both. [web.pages.<key>] sets one page's title and description, which fall back to the site's, and index, which defaults to true. A page with index = false carries <meta name="robots" content="noindex"> and is left out of the sitemap; it keeps its canonical, Open Graph and hreflang metadata.

See the web guide.

[dependencies]

The runtime capabilities the app declares, brought in at startup. A name is answered by a module the binary was built with, or by a library beside the app; one table covers both runtime kinds, and the kind is detected at load from the symbols the file exports, never declared:

  • A runtime module: an engine-locked Rust dylib with full ECS reach. Its entries carry the name the table key gives it, so a module found under another name is not loaded.
  • A portable plugin (lumen_plugin_v1): a C-ABI library offering script functions, language preludes, and events.
  • A candela script library: .cdl sources the app's own scripts import. It is not a library the engine opens; it compiles into the app.

One table entry per library; the key is its name.

[dependencies]
lumen-archive = { bundled = true }
lumen-audio = { bundled = true }
lumen-canvas = { bundled = true, tags = ["canvas"] }
lumen-fs = { bundled = true }
markdown-widgets = "1.2"
shape-tools = { path = "modules/shape-tools", config = { units = "mm" } }

The first-party modules ship with the toolchain, so bundled = true is all they need; see runtime modules for getting them onto a machine:

Module What it adds Config
lumen-archive The archive script namespace: unpacking zip, tar, and gzip-compressed tar into a directory, off the tick loop. max_concurrent
lumen-audio The whole audio surface, from the audio_* script functions to the playback backend behind them.
lumen-canvas The <canvas> element and the canvas script namespace that draws on it. Declare tags = ["canvas"] alongside it. region_cap, buffer_pixel_cap, buffer_count_cap
lumen-download The download script namespace: fetch a URL to a file off the tick loop, reporting progress, completion, and failure as events. timeout_ms, max_bytes, max_concurrent
lumen-fs The files script namespace: read, write, list, copy, remove, and byte-level file access, resolved against the app directory. read_bytes_cap
lumen-process The process script namespace: start another program, and take its output a line at a time and its exit as events.

A build that compiles a declared module in answers the name from that copy and opens nothing. A name it neither compiles in nor can open beside a shared engine is skipped with a notice.

Each entry declares exactly one source:

Key Type Effect
bundled true The library ships with the toolchain; the runtime looks beside the running engine (the executable's directory, then LUMEN_LIB_DIR, then a modules/ directory beside either).
version string A requirement on a registry package, in cargo semantics ("1.2" means ^1.2). Every compile path resolves it and downloads what it resolves to; the runtime never fetches or resolves one itself, it only loads what was resolved or already staged in a modules/ directory, and fails with a banner otherwise. A bare string value (name = "1.2") is shorthand for this key.
path string A built library, relative to the app directory unless absolute. Without an extension the platform spellings are probed (lib<m>.so, lib<m>.dylib, <m>.dll - the Windows spelling matters for portable plugins, the kind that loads there - plus the underscored variants cargo produces for a hyphenated name).
config table Handed to the library verbatim at install.

A module that brings a markup element declares it too:

Key Type Effect
tags array of strings Markup tags this module answers for, so the parser accepts them. Lowercase letters, digits, and dashes; a tag the language already owns is refused.

A compile loads nothing, so lumenc build, lumenc check, and lumenc package know an element exists only because the app said so. A run also learns it from the module itself, which registers its tags as it installs - so an app missing the key runs and fails to build. Declare it either way and both paths agree.

The table is unordered, so entries load in sorted-name order; where an entry sits in the file carries no meaning.

Each kind carries its own handshake, verified at load. A runtime module read off disk is version-locked to the exact engine build it was compiled against and opens only on Linux and macOS, into a dynamically linked engine; a module compiled into the executable has nothing to verify and answers on every platform. Windows only has the second shape, which is what lumenc package --static produces, so a Windows package of an app declaring this table is that one. A portable plugin is checked against the plugin ABI version and the script wire version, and loads on every desktop platform, static builds included. Any failure - a missing file, a failed handshake, a library exporting neither entry symbol (the banner names both; a compiler plugin is pointed at [[plugins]]) - is an unmissable stderr banner naming the entry and the reason, and the app starts without it. One case is quieter: a runtime module a statically linked build neither compiled in nor can open is skipped with a single stderr line rather than the banner, because that is a property of how the binary was put together; the line points at lumenc package --static, which compiles the declared modules in. The modules Lumen ships are not in that case: lumenc run carries them, so an app declaring one runs with it during development on every platform. lumenc bundle --static says the same thing at build time, naming the declared modules. lumenc web refuses an app that declares a native library here, because a browser cannot load one; a candela package is script source, so it travels to the web like the app's own scripts.

A declared library is native code loaded into the app's process, the same trust model as [[hooks]]. A permissions key is reserved and rejected; capability declarations are not supported yet. git, rev, and registry sources are reserved and rejected the same way.

Registry packages

A version source in [dependencies] or [[plugins]] names a package published to the registry at reg.lumenfx.dev. lpm is the client that resolves those requirements and downloads what they resolve to; every compile path runs it before it compiles, so nothing has to be fetched by hand. See the CLI reference for add, remove, fetch, and update.

A package declares the platform it is for, and the table that named it says what the app wants it for:

Platform Table What it is
lumen [dependencies] A runtime module or a portable plugin, told apart at load by the symbols it exports.
lumen [[plugins]] A compiler plugin, opened while the app compiles.
candela [dependencies] A script library. Its .cdl sources become an import root under the declared name, so import "shapes"; in the app's script reads the package.

A package for any other platform is an error naming both the package and the platform.

Windows is the one gap: an engine-locked runtime module from the registry does not load there, because there is no shared engine for one to load into. Portable plugins, compiler plugins, and candela packages work everywhere.

lumen.lock

lpm writes lumen.lock beside lumen.toml, recording the exact version every requirement resolved to. It is format version 2, and lumenc never reads or writes it: the file belongs to lpm.

Commit it. A build from a fresh clone then resolves to the versions the lock names rather than to whatever is newest, which is what makes two machines build the same app. lumenc update is how a pin moves; editing the lock by hand is not.

[[hooks]]

Build and setup commands the app declares for itself. Each [[hooks]] entry is one command.

Key Type Required Effect
when "prebuild", "prerun" yes Trigger point.
run string yes The command line. Must not be empty or whitespace-only.
os "linux", "macos", "windows" no Restricts the hook to one platform. Absent runs everywhere.
inputs array of strings no Files the command reads. Used only for the staleness check.
outputs array of strings no Files the command produces. Used only for the staleness check.
[[hooks]]
when    = "prebuild"
os      = "linux"
run     = "mkdir -p lib && cc -shared -fPIC -O2 -o lib/libmd.so md.c"
inputs  = ["md.c"]
outputs = ["lib/libmd.so"]

The output lands in lib/, the directory a script's dylib import loads shared libraries from (see the project layout).

prebuild hooks fire for lumenc run, build, and bundle. prerun hooks fire for lumenc run only, after every prebuild hook. lumenc check never runs hooks.

Matching hooks run in declaration order with the app directory as the working directory, through sh -c on Linux and macOS and cmd /C on Windows. A hook that exits non-zero aborts the command; later hooks do not run.

A hook is skipped when both inputs and outputs are non-empty, every listed file exists, and every output is at least as new as the newest input. A hook missing either list, or one whose listed files are not all on disk, always runs.

Hooks run shell commands read from a file in the app directory, the same trust model as a Cargo build script. lumenc run --no-hooks skips them.

An unknown when or os value, or an empty run, is a parse error naming the offending value.

[[plugins]]

Compiler plugins the app declares. Each entry is one plugin; entries run in declaration order on every compile path (run, build, check, package, web). See authoring compiler plugins for what a plugin can do.

Key Type Required Effect
name string yes Plugin name; the loaded library must report the same one.
version string one source A requirement on a registry package, in cargo semantics ("1.2" means ^1.2). Fetched before the compile and pinned in lumen.lock.
path string one source A built cdylib, relative to the app directory (absolute paths work too). Without an extension the platform spellings are probed (lib<p>.so, lib<p>.dylib, <p>.dll, plus the underscored variants cargo produces for a hyphenated name).
config table no Handed to the plugin verbatim; a key the plugin does not read produces no diagnostic.
[[plugins]]
name    = "markdown"
version = "1.2"
config  = { flavor = "gfm" }

[[plugins]]
name = "local-dev"
path = "plugins/local-dev"

Declare exactly one source per entry. git, rev, and registry sources are not supported yet and error saying so, and a permissions key is rejected the same way: a plugin is native code running in the compiler's process, the same trust model as a build script.

A version source resolves to a prebuilt, per-platform cdylib the registry publishes, downloaded before the compile that needs it and pinned in lumen.lock. lumenc add markdown --plugin writes the entry and resolves it in one step.

Unlike [[hooks]], plugins also run under lumenc check, so the tree being validated is the tree a build produces; emit outputs are discarded there, though a version source may still write lumen.lock. A plugin is native code loaded into the compiler's process, the same trust model as a Cargo build script.

[signals]

An optional typed schema for the app's signals, read by lumenc lint --signals. Nothing else consumes it, and a missing entry is not an error; it downgrades the lint severity.

Each key maps a signal name to a type: i64 (also int, integer), f64 (also float, number), bool (also boolean), string (also str, text), color, vec2, array, or object (also map). An unknown type is a parse error.

A value can also be a table. An inline table with no type key is an object whose entries are its field types. An explicit type = "array" or type = "object" with a fields table types the record at the leaf.

There is no signal_set_string; signal_set is already the typed write for a string-declared signal, so the lint's untyped-write finding does not fire on it.

[signals]
count = "i64"
theme = "string"
user = { name = "string", email = "string" }

[signals.users]
type = "array"
fields = { id = "i64", name = "string", email = "string" }