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


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
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:

```lma
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.** `DESCRIBE` surfaces 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.** `EACH` fans one call over one list. Anything more wants `lmx`.
- **Register anything.** Every plane is read-only from here.
- **Run from the CLI.** No `tools` bridge there.

---

## Running it

```bash
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`.


---
Source: .claude/skills/leumas-lmx/reference/lma.md
Canonical: https://docs.leumas.tech/p/skills/leumas-lmx/lma
