Skip to content

lm — CLI & TUI reference

lm is the integrator's single binary: a kubectl-style CLI plus a k9s-style TUI. This page maps the whole surface; every command also self-documents via lm <cmd> --help.

Global flags on every command: --context / --server / --token (override the current context), -o table|json|yaml, -v. --token is sent as-is: the context's stored session and its refresh are skipped for that command, so a token from elsewhere works even when the stored one has expired. (Three exceptions redefine -o as an output file path: lm source rest from-openapi, lm view get, lm view generate.)

Context & auth

Command What it does
lm config set-context NAME --server URL --oidc-issuer URL create/update a context (--oidc-client-id defaults to lm-cli, the bundled realm client)
lm config use-context NAME switch context
lm config view print ~/.lm/config.yaml
lm login OIDC device flow: prints a URL + code, stores tokens in the context
lm version binary version

First run? The deploy page walks through standing up a hub and pointing lm at it.

Environment & TLS

Two env vars override fields of the resolved context — CI-friendly, no lm login needed: LUMNIK_URL replaces the server, LUMNIK_TOKEN the Bearer token. A context must still exist and be current (lm config set-context ci --server … && lm config use-context ci, once); the env vars swap its values per run, they don't replace it.

Two TLS fields are hand-edited in ~/.lm/config.yaml (not set-context flags): ca-file: /path/to/root.crt trusts an internal CA (the TLS façade's LAN certificate), insecure-skip-verify: true disables verification entirely. When both are set, ca-file wins — trusting a CA is the safer intent.

The tenant: / user: context fields exist too, but they only work against dev-bypass hubs (sent as X-Tenant-ID / X-User-ID headers); a real hub ignores them.

Manifests (all kinds)

Command What it does
lm validate -f FILE validate without applying — field-by-field errors, exit code 1 on error
lm apply -f FILE apply a kind: Connector manifest — upserts by (tenant, metadata.name), so re-applying an edited file updates it. Not to be confused with lm connector apply, which takes those flat stored fields instead
lm entity apply -f FILE apply a kind: Entity fusion manifest
lm entity delete NAME remove an entity — drops its view, deletes the manifest, rediscovers its scopes (lm_admin; the inverse of entity apply). This is what frees a connector blocked by the 409 below
lm entity export NAME -o FILE [--force] download the entity's canonical CSV snapshot through the one-shot door — no kind: Export needed. The file appears only once the whole body has arrived, an existing file is refused without --force, and -o - streams to stdout
lm app apply -f FILE declare a kind: App métier app — title, scope, icon and theme; prints the app and, when it takes the default flag, the app that lost it
lm process apply -f FILE / lm process list apply/list kind: Process notify rules
lm process enable/disable NAME stop a process firing, or start it again — the manifest stays stored and re-appliable
lm process rm NAME delete a process for good (no prompt); 404 if the tenant has no process by that name
lm workflow apply -f FILE apply a kind: Workflow lifecycle — prints the data-truth report
lm describe connector-type KIND field reference for a connector kind

Connectors, endpoints, sources

Command What it does
lm connector list/get/apply/delete manage connector instances. connector apply -f is not lm apply -f: it takes the FLAT stored fields (tenantId, name, typeId, schedule, enabled, config) — the shape lm connector get writes — and is how you edit one field without rewriting a manifest; lm apply -f takes a kind: Connector manifest. Handing a manifest to connector apply is refused with the command that opens. delete erases the connector's data: its mirror tables (connector.t_* and registered REST ext.*) are dropped, registry rows removed, and every RAG chunk it produced dies with it — this is the erasure gesture. A one-shot's ext.* table goes the same way. A table a fused entity depends on blocks the delete with a 409 naming the entity — lm entity delete NAME frees it
lm connector run NAME trigger an immediate run
lm connector pause/resume NAME stop/restart scheduling
lm connector scope ls/set/add/rm/sync manage the connector's métier scopes; sync re-stamps its already-indexed chunks
lm endpoint enable/disable/reset-cursor/purge C/E per-endpoint scheduler + cursor; purge deletes the endpoint's ext.* rows (--force skips the prompt)
lm source rest test/probe/from-openapi test C pre-flights every ENABLED endpoint — the request a run would send, nothing written — and exits non-zero if one does not answer with readable data; probe C --endpoint PATH samples that one path on connector C — its connection and its stored credential — and drafts the endpoint fragment to paste under spec.endpoints; from-openapi URL --name N generates one (--scope repeatable, -o FILE writes to a file, --apply upserts straight to the hub). source rest run is a stub — use lm connector run
lm source jdbc test/discover/add open a JDBC source directly; the password is prompted at a keyboard (never echoed) or read from stdin — --password-stdin declares the pipe, and an undeclared one warns. There is no --password
lm source jdbc list-tables NAME list the tables registered under a source the hub already holds (no credential)
lm source schema --scope TAG / lm source rediscover --scope TAG show / rebuild the scope's SchemaCard (--scope is required)
lm csv ingest FILE\|sftp://…\|s3://… one-shot CSV ingest (dormant connector, ext.* table; askable at once in the scope you pass, no run recorded) — full SFTP/S3 flag surface: see CSV & files
$ lm connector list
NAME          TYPE      TENANT  SCHED  WINDOW  STATUS     LAST RUN
customers     csv-file  1                      Completed  2026-09-05 05:38
orders        csv-file  1                      Completed  2026-09-05 05:38
products      csv-file  1                      Failed     2026-09-05 05:38
$ lm csv ingest stock.csv --scope retail --name stock-count
Ingested stock.csv into ext.stock_count
  • rows written:    3
  • promoted cols:   4
      - sku (text)
      - label (text)
      - qty (bigint)
      - warehouse (text)

  • askable:         scope 'retail' — try: lm ask --scope retail "how many rows are in ext.stock_count?"
A dormant connector is registered — see `lm connector list`.

Data, runs & answers

Command What it does
lm run list/get ID connector runs, one row per endpoint (records in/out/skip, per-run transparency) (list: newest first, --connector NAME, --limit N)
lm run cancel ID request cancellation of a Running run — cooperative, not immediate (id resolves from an 8-character abbreviation, same as get); refused honestly rather than silently, see Stopping a run for both refusals and the retry advice
lm dlq list/get ID dead-letter entries (reason + offending row) (--run takes the run's id or its 8-char abbreviation; --connector takes the connector's id or name; list --limit N)
lm dlq replayed/discard ID mark an entry handled (replayed --new-run-id RUN links it to the run that replayed it)
lm conflicts list fused-entity rows whose sources still disagree (--limit N)
lm observations list the workflow tape: watched documents, stillest first (--limit N)
lm observations stats the tape aggregated per workflow: watched, dormant, median detection delay (seconds; - when nothing is dormant)
lm integration add NAME --url U --events T [--secret-stdin] register a webhook target — the secret arrives on stdin, never a flag; forwards events from registration onward
lm integration list / describe ID registered targets: ID, name, URL, events, enabled
lm integration test ID [--event T] fire a catalog event at the target and report the delivery (decision.* types are dynamic — verify those with a real decision + lm outputs list)
lm integration rm ID remove a target — delivery history is kept
lm notify add --events T --to EMAIL [--template N] / list / rm ID email rules: events matching the pattern go to the recipient (delivery requires the deploy's SMTP knobs — see the self-host guide)
lm outputs list what the substrate sent out and whether it arrived, failures first (--limit N)
lm data tables / lm data row HASH dynamic tables; the raw row behind a citation
lm mapping list/get/apply mapping templates + per-tenant overrides (get --raw = the raw template YAML; apply -f FILE)
lm secret set/list/describe/rm encrypted-at-rest secrets; values never returned. set NAME prompts without echo; set NAME - reads stdin until EOF (a multi-line key, or a script); set NAME VALUE still works but warns — that value is in your shell history. Also set --description "…", rm --force. rm does no reference check: a connector still naming the secret fails against its source at the next run
The two tables you will read most:
$ lm run list --connector orders
ID        CONNECTOR  ENDPOINT  STATUS     IN  OUT  SKIPPED  DEAD-LETTERS  STARTED           ENDED
f0466804  orders     default   Completed  10  8    2        2             2026-09-05 05:38  2026-09-05 05:38
$ lm dlq list --run f0466804-0824-4eb4-8aa2-54e263dccafe
ID        CONNECTOR  RUN       REASON           ATTEMPTS  MESSAGE
66384fc7  487ed9d1   f0466804  MalformedSource  1         /tmp/exports/orders.csv:9 — Index for header 'status' is 2…
c58de40c  487ed9d1   f0466804  MalformedSource  1         /tmp/exports/orders.csv:8 — Record has 6 values but only 5…

The ID columns are abbreviated — and the abbreviation works

lm run list and lm dlq list shorten ids to their first 8 characters so the table fits a terminal. What they show is what get, discard and replayed accept: paste the 8 characters back and the id is resolved for you. An ambiguous prefix is refused rather than guessed, and -o yaml prints every id in full. The --run and --connector filters resolve the same way: --run accepts the 8-character abbreviation, and --connector accepts the connector's name.

A full page says so — and --connector asks the hub, not the page

Six commands answer in pages: lm run list, lm dlq list, lm conflicts list, lm observations list, lm outputs list and lm inbox. Each takes --limit N, and each door has its own ceiling — the run and dead-letter ledgers and the inbox stop at 200, deliveries at 500, conflicts and the workflow tape at 1000.

When a page comes back full, the command prints note: result hit the limit of N — there may be more on stderr; the table itself stays on stdout, so a pipe, -o json and -o yaml are unaffected. It says may because a page that fits exactly looks the same from outside. Raise --limit to see further back.

--connector on lm run list is part of the question the hub is asked, not a sieve over the page that came back. That is what lets you find a connector that has not run for a while on a busy hub: its runs are looked up among its own, so a quiet connector never comes back as an empty table just because newer runs filled the page.

Ask your data

Command What it does
lm ask [--scope S] "question" analytic text-to-SQL over the fused hub — prints the answer, a data as of line (the scope's last completed run) and the SQL that ran (the five guards apply); a scope-bound user must name a granted scope or gets 403 scope not granted — without --scope, asks your default (lm scope set TAG; same 403 if not granted)
lm chat interactive REPL chat — in-REPL commands: :corpus NAME (or all), :scope TAG (or default), :topK N, :clear, :help, :exit
lm scopes / lm scope / lm scope set TAG list scopes (a scope-bound user sees only their granted métiers); show/set your default (404 no user record on this hub… means your login points at a hub user id that does not exist — Tenants, users & scopes)
lm rag search/status/reindex semantic search (--top-k N, default 5); corpus status; re-index of the platform corpus (manifests/API/hooks — ingested-row chunks are written at ingestion and erased with their connector)

The apps ladder

Command What it does
lm view -f FILE [--filter …] render a kind: View list — filters: col=v · col~text · col>v (>= < <=, number/ISO date); repeatable (AND). The value is a bare literal: --filter 'city=Lyon', and a quoted one (city='Lyon') is refused rather than matched against the quotes
lm view KEY -f FILE one record's detail (with the _conflicts ledger)
lm view generate SCOPE "DESCRIPTION" [-o FILE] ask the AI to write a kind: View manifest over a scope; review, then render with lm view -f. Refusals (abstention / failed generation) surface verbatim as errors
lm view list [--scope S] list the saved views visible to the caller (scope-bound reads apply)
lm view get NAME [-o FILE] fetch one saved view's manifest YAML — to stdout, or to a file
lm view apply -f FILE validate and upsert a kind: View manifest into the saved-view store
lm app apply -f FILE declare a kind: App métier app — title, scope, icon and theme; prints the app and, when it takes the default flag, the app that lost it
lm workflow apply -f FILE declare a kind: Workflow lifecycle; prints the data-truth report — values in the column the manifest never declared, and declared states never seen. Never blocks
lm workflow list the tenant's declared lifecycles (name, table, column, scope)
lm workflow get NAME fetch one workflow's manifest YAML to stdout (redirect with > to save; the global -o here means output format, it does not write a file)
lm export apply -f FILE declare a kind: Export — entity, columns, format, destination, optional schedule; prints the stored name, entity and scope (-o json for scripts). Inline credentials are refused by the hub, naming the field only
lm export list / lm export get NAME the exports your scopes can see; one export's stored manifest YAML (a URL's user:password@ is printed as ***@)
lm export run NAME [--wait] start a managed run. Without --wait it answers once the hub has claimed the run (Running) — not delivered. --wait follows the ledger to Completed (prints the receipt) or Failed (exit 1 with the hub's failureCode and reason); --timeout (default 30m) and an interrupt stop the waiting, never the run
lm export retry NAME RUN_ID [--wait] a new attempt of a Failed run — same artifact name, regenerated from byte 0
lm export runs NAME [--limit N] / lm export status NAME [RUN_ID] the export's run ledger, newest first; one run in full — its latest when no id is given. Short ids from runs are accepted
lm inbox [--limit N] [--all] recent decision.* events — what needs you
lm inbox ack ID dispose of a decision (idempotent — the ack is an event)

Admin & platform

Command What it does
lm module list installed business modules
lm migration status/apply Flyway status per schema (apply --schema core\|identity\|… — required)
lm hook list/get/apply/delete · reload NAME Kotlin platform hooks (hook generate is a v1.x stub)

The TUI

Bare lm (or lm ui) launches the k9s-style TUI. : opens the command palette.

Full tour by task, plus the complete key reference, on The TUI.