lmg
Write a Leumas programming grid as text and run it. Safe mode, no bridge required — it works from the CLI, the API and the Playground alike.
lmg — the grid-authoring language
Write a Leumas programming grid as text and run it. Safe mode, no bridge required — it works from the CLI, the API and the Playground alike.
GRID "pricing"
LAYER default
A1 variable 1200
A2 variable 0.08
B1 code $A1 * (1 + $A2)
C1 conditional $B1 > 1000 ? "high" : "low"
D1 template "tier {{ $C1 }} at {{ $B1 }}"
RUN B1, C1, D1
GRAPH
grid "pricing" — 1 layer(s), 5 cell(s)
default.B1 = 1296
default.C1 = high
default.D1 = tier high at 1296
--- dependency order ---
default.A1
default.A2
default.B1 ← default.A1, default.A2
default.C1 ← default.B1
default.D1 ← default.C1, default.B1
What a grid actually is
Not a spreadsheet. A grid is layers of cells, and a cell is an action. A1 is not a number in a box; it is a variable action that produces one. B1 is a code action. There are ~21 cell types.
Cells reference each other with $refs, and @leumas/grids owns everything hard about that: it resolves refs concurrently, memoises so asking twice costs one run, checks for cycles, and evaluates code cells in a node:vm sandbox with no process, require or fetch.
lmg writes no evaluator. It is a syntax for that document, and the document it produces is the same shape the Studio grid editor persists.
Syntax
| Line | Meaning |
|---|---|
GRID "name" | names the grid (optional) |
LAYER <name> | starts a layer; a name may contain spaces |
<REF> <type> <rest> | a cell |
<REF> <type> do … end | a cell whose body spans lines |
RUN a, b, c | execute and print those cells |
DUMP | print every cell computed so far |
GRAPH | print the dependency order |
# starts a comment — except inside a do … end body, which is taken verbatim. A # in a code cell is JavaScript, and eating it would corrupt exactly the cells most likely to have one.
Cell refs are uppercase letters then digits (A1, B12, AA3) — that is the ref grammar @leumas/grids defines, not a choice made here. Cells declared before any LAYER go into a layer called default.
The one parsing rule
After <REF> <type>:
- if the rest of the line starts with
--, it is parsed as flags and nothing else; - otherwise the entire rest of the line is the cell's primary field,
--and all.
So B1 code $A1 - -2 means what it looks like, and B1 json --input=$A1 --op=get --path=name also means what it looks like. Nothing has to guess.
Flag values are JSON-coerced (--limit=5 is the number 5, --keys=["a"] is an array), and a bare --flag is true.
$refs
| Form | Resolves to |
|---|---|
$A1 | A1 in the same layer as the cell writing it |
$default.A1 | A1 in the layer literally named default |
$Layer 2.B2 | B2 in the 2nd layer, 1-based |
RUN targets are different: a bare RUN B1 has no owning layer, so it resolves to the layer that declares B1. If two layers declare it, lmg refuses and asks you to qualify it rather than picking one.
Cell types
Every type in allCellAdapters is available. The ones with a bare-rest-of-line form:
| Type | Bare form fills | Example |
|---|---|---|
variable / value | value | A1 variable 1200 |
code | code | B1 code $A1 * 2 |
conditional | see below | C1 conditional $B1 > 100 ? "hi" : "lo" |
template | template | D1 template "tier {{ $C1 }}" |
aggregate map transform switch json csv regex validate hash | input | E1 json $D1 |
merge random | mode | F1 random float |
datetime | op | G1 datetime now |
state | key | H1 state cart |
endpoint url | url | (needs LMX_ENABLE_NET) |
media | items |
Any other type still works — name its fields with flags. A type with no bare form is an error rather than a guess, because a wrong guess runs.
conditional sugar
C1 conditional $B1 > 1000 ? "high" : "low"
desugars to --condition="$B1 > 1000" --trueResult=high --falseResult=low. The explicit flags are accepted too. ? and : inside quotes are ignored, so conditional $A1 = "who? me:" ? "yes" : "no" splits where you expect.
template slots take $refs, not bare names
D1 template "tier {{ $C1 }} at {{ $B1 | fixed:2 }}"
{{ C1 }} without the $ is not a ref — it renders the literal text C1. Helpers after | come from the template adapter: upper, lower, json, fixed:n, default:x.
[critical] The network gate
Two cell types make outbound requests: endpoint and url. Every other LMX mode that can reach the network (lmnet) is admin-gated behind LMX_ENABLE_NET, so lmg — a free mode — cannot be the open door to the same reach.
Without LMX_ENABLE_NET=1 those two adapters are replaced, not removed, and fail with:
"endpoint" cells reach the network — set LMX_ENABLE_NET=1 to allow them.
Removed, they would read as "unknown cell type", which sends the author hunting for a typo instead of at the policy.
The privileged lmx, adapter and exec cell types that leumas-api injects into its own grids domain are also out of reach here: a free script must not reach the ecosystem through a cell when it cannot reach it through adapter().
One implementation detail worth knowing
lmg writes each cell's options into specialConfig, and a code cell's source into a top-level code field — not into props.
Every adapter merges props over specialConfig, so either would execute correctly. But buildDependencyGraph reads only code and specialConfig. A props-authored grid therefore runs perfectly and reports an empty dependency graph — GRAPH prints every cell with no edges, and nothing in the output says it is lying. Writing specialConfig also makes an lmg grid identical to one the Studio editor produced.
What it deliberately cannot do
- Open or save a stored grid. Inline grids only, so there is no ownership question and no write
path around /api/grids and its PassNode meter.
- Reach the ecosystem. No
lmx/adapter/execcells. - Reach the network without the flag.
Running it
leumas-lmx ./pricing.lmg
curl -s -X POST localhost:3000/api/lmx/run \
-H 'content-type: application/json' \
-d '{"mode":"lmg","source":"LAYER default\n A1 variable 4\n B1 code $A1 * 3\nRUN B1"}'
Bundled examples: shared/engines/lmx/src/scripts/demo.lmg, shared/engines/lmx/src/scripts/pricing-model.lmg. Tests: shared/engines/lmx/src/test/lmg.test.js. Guard: pnpm smoke:lmx.