lma
Call anything this install has. Safe mode, needs the host's tools bridge — so POST /api/lmx/run or Studio's LMX Playground, not the CLI.
lma — the any-tool language
Call anything this install has. Safe mode, needs the host's tools bridge — so POST /api/lmx/run or Studio's LMX Playground, not the CLI.
PLANES
DISCOVER "base64"
DESCRIBE a-text.Base64Encode
USE a-text.Base64Encode { "text": "{{input}}" } -> encoded
PRINT "encoded: {{encoded}}"
TRACE
RETURN encoded
The idea: there is no verb table
Every other LMX language has a vocabulary someone typed into a file. lmq knows FROM. lmai knows ASK. Add an adapter to Leumas and neither gains anything.
lma has no verbs of its own. Its vocabulary is read from live registries on every single call:
| Plane | Prefix | Where the verbs come from |
|---|---|---|
| Tools | (none — default) | toolRegistry() — adapter tools, stored functioncalls, user integrations, grid cells |
| Actions | action: | actions.listManifests() / actions.get(id).run(props) |
| MCP | mcp: | the MCP server map + executeTool |
| Capabilities | cap: | CAPABILITY_LIST + the connectors runCall pipeline |
Register an adapter, write a functioncall in Studio, connect an integration, publish an app — and every existing lma script can call it. Nothing is rebuilt, no language code changes, no redeploy. The planes are read as getters, never memoised, precisely so a script cannot be pinned to the vocabulary that existed at boot.
cap: is the interesting one. A capability names an outcome, not a vendor: cap:email.send resolves at call time to whichever connected provider declares it fulfills that capability, using whichever of this caller's credentials apply. Connect a new provider and scripts that already asked for email.send start working, with nothing edited.
Statements
# starts a comment.
| Statement | Does |
|---|---|
PLANES | what planes this install has, and how many verbs each holds |
DISCOVER "query" | search every plane; empty query lists everything (capped at 40) |
DESCRIBE <ref> | one verb's description and declared parameters |
USE <ref> { …args } -> var | call it; { … } and -> var are both optional |
EACH <item> IN <list> USE <ref> { … } -> var | fan the call over a list, 4 at a time, order preserved |
SET <name> = <value> | a variable; JSON-coerced |
WHEN <cond> THEN <statement> | one guarded statement |
PRINT <path> · PRINT "… {{slot}} …" | |
TRACE | every call made so far — ref, ok, ms, metered |
USING <ref>, <plane>, … | narrow this script's own scope |
RETURN <path> | the script's value; stops the script |
References
a-text.Base64Encode the default (tool) plane
action:notify an automation action
mcp:github.create_issue <server>.<tool>
cap:email.send a vendor-neutral outcome
{{slots}}
Arguments are a JSON literal, filled from inputs, SET variables and prior results before being parsed. That ordering is what lets a slot carry a whole structure:
USE a.b { "rows": {{results}} } → { "rows": [{…}, {…}] }
An unresolved slot is an error, and a literal that stops being valid JSON after filling is an error — never a silently-empty {}, which would be a tool call that runs and does the wrong thing.
What bounds a script
| Gate | Effect |
|---|---|
| Host scope | The mounting app may hand each caller an allow-list. Out-of-scope verbs are invisible to DISCOVER and DESCRIBE, not merely unusable. |
USING | The script's own narrowing. It can only ever narrow further, never widen. |
| Call budget | 50 USE calls per script — a loop cannot drain a wallet. |
| Metering | Each call charges passnode.charge({ target: 'tool:<ref>' }). |
| Timeout | 30s default. |
[critical] On metering
POST /api/adapters/:system/:fn is priced by passnode.guard. That guard lives in the HTTP router — registry.run() itself is not metered, so every non-HTTP caller of an adapter tool has always been free, the in-script adapter() builtin included.
That is a small gap while scripts call a tool occasionally and a large one for a language whose entire purpose is calling tools. lma therefore charges the same tool: target string the HTTP route charges, so a script and an HTTP client are priced alike. A refused gate stops the call before the tool runs.
(The older adapter() builtin remains unmetered. Changing it would alter the behaviour of existing scripts, so it is left alone and flagged here instead.)
TRACE, and why it exists
A language whose verbs resolve at run time has to be able to say what it actually called:
--- trace ---
✓ a-text.Base64Encode 3ms · metered
✓ cap:email.send 210ms
For a cap: ref this is the only record of which provider served the call — the script named an outcome, and the ecosystem chose. Without it a run is not reproducible.
What it deliberately cannot do
- Validate your arguments.
DESCRIBEsurfaces the declared schema for a human; nothing enforces
it. A registry's schema describes intent, and rejecting a call against a stale or partial one turns a working tool into a broken language feature.
- Loop, or define functions.
EACHfans one call over one list. Anything more wantslmx. - Register anything. Every plane is read-only from here.
- Run from the CLI. No
toolsbridge there.
Running it
curl -s -X POST localhost:3000/api/lmx/run \
-H 'content-type: application/json' \
-d '{"mode":"lma","source":"PLANES\nDISCOVER \"base64\""}'
Bundled examples: shared/engines/lmx/src/scripts/demo.lma, shared/engines/lmx/src/scripts/summarize-rows.lma. Tests: shared/engines/lmx/src/test/lma.test.js. Guard: pnpm smoke:lmx.