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 documents itself with lm <cmd> --help.
Every command takes the global flags:
$ lm --help
…
Flags:
--context string Override the current context from ~/.lm/config.yaml
-h, --help help for lm
-o, --output string Output format: table|json|yaml (default "table")
--server string Override the server URL
--token string Override the auth token (sent as-is; the context's refresh is not run)
-v, --verbose Verbose output (debug logs)
--tokenskips the context's stored session and its refresh, so a token from elsewhere works even when the stored one has expired.- A value written on several lines (a memo, say) shows on one line in a table, the break written
\n.-o jsonand-o yamlgive it as it is. - Five commands use
-ofor an output file instead:lm source rest from-openapi,lm view get,lm view generate,lm entity exportandlm proposal draft.
A command that only groups others (lm connector, lm entity, lm module…) prints its subcommands
when typed alone. A word that is not one of them is an error with a non-zero exit:
$ lm connector lst; echo "exit=$?"
Error: unknown command "lst" for "lm connector"
exit=1
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 set-context NAME --server URL --tenant ID --user ID |
a context for a dev-bypass hub (make k8s-up): the ids are sent as X-Tenant-ID / X-User-ID; an OIDC hub ignores them |
lm config use-context NAME |
switch context (the default for every later command; inside lm ui, :context switches the session only) |
lm config view |
print ~/.lm/config.yaml with each credential shown as REDACTED — safe to paste into a ticket; --raw prints the tokens in clear |
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
| Setting | Where | Effect |
|---|---|---|
LUMNIK_URL |
env var | replaces the server of the context in use (current, or --context), per run; --server wins over it |
LUMNIK_TOKEN |
env var | the Bearer token, per run — for CI. Sent as is, ahead of a stored lm login session, never refreshed or saved; --token wins over it |
ca-file: /path/to/root.crt |
~/.lm/config.yaml, by hand |
trust an internal CA (the TLS proxy's LAN certificate) |
insecure-skip-verify: true |
~/.lm/config.yaml, by hand |
disable TLS verification; ca-file wins when both are set |
tenant: / user: |
~/.lm/config.yaml |
dev-bypass hubs only; a real hub ignores them |
The env vars need an existing context: create it once with
lm config set-context ci --server … && lm config use-context ci. set-context also takes
--token, stored in the context as it is.
Manifests
| Command | What it does |
|---|---|
lm validate -f FILE |
validate a kind: Connector manifest without applying it — field-by-field errors, exit code 1 on error. Other kinds are checked by their own apply |
lm apply -f FILE |
apply a kind: Connector manifest — upserts by metadata.name, so re-applying an edited file updates it. Prints Applied connector "NAME" — 1 endpoint; -o json prints the hub's answer, the connector id included. Not lm connector apply, which takes the flat stored fields |
lm entity apply -f FILE |
apply a kind: Entity fusion manifest |
lm entity delete NAME |
remove an entity and its view (lm_admin). Names the view and the search indexes over it, then asks you to type delete (--force skips the question). This is what frees a connector blocked by the 409 below |
lm entity list |
the entities you may read — name, sources, scopes, match or resolution |
lm entity get NAME [--filter COLUMN=VALUE]… [--limit N] [--offset N] |
one page of an entity's canonical rows (Read API). --filter is an exact match — a date on a date column is equal as an instant — repeatable; --limit is 1..1000 (default 100); a full page is announced on stderr |
lm entity resolve NAME |
resolve a resolution: entity over every row now (lm_admin); prints what was resolved, ambiguous and unresolved |
lm entity ambiguous NAME [--limit N] [--offset N] |
the rows a resolution: entity could not decide, with their candidates |
lm entity history NAME SOURCE ID |
every decision about one source row: outcome, rule, candidates, automatic or asserted |
lm entity assert-same\|assert-distinct NAME --left SOURCE:ID --right SOURCE:ID [--reason TEXT] |
a correction that outranks every rule, applied at the next resolve (lm_admin) |
lm entity export NAME -o FILE [--force] |
download the entity's canonical CSV through the one-shot door. An existing file is refused without --force; -o - streams to stdout |
lm app apply -f FILE |
declare a kind: App business app — title, scope, icon and theme |
lm app list / lm app get NAME |
the declared apps; one app as an applyable manifest (lm app get NAME > app.yaml, edit, re-apply) |
lm app rm NAME |
delete a declared app for good (no prompt); the last default app is refused |
lm view apply -f FILE / lm view list / lm view get NAME / lm view rm NAME |
saved kind: View manifests: store, list, fetch, delete (no prompt) |
lm process apply -f FILE / lm process list |
apply/list kind: Process notify rules |
lm process enable/disable NAME |
start or stop a process firing — the manifest stays stored |
lm process rm NAME |
delete a process for good (no prompt) |
lm workflow apply -f FILE |
apply a kind: Workflow lifecycle — prints the data-truth report |
lm workflow list / lm workflow get NAME |
declared lifecycles (name, table, column, scope, enabled); one manifest's YAML on stdout |
lm workflow enable/disable NAME |
serve a workflow again, or stop serving it — the manifest stays stored |
lm workflow rm NAME |
delete a workflow for good (no prompt) |
lm export apply -f FILE |
declare a kind: Export |
lm proposal draft DOSSIER |
write a draft kind: Workflow from a proposal dossier (- for stdin); flags --name --scope --table --column --key --after -o. Offline, never applies; -o FILE refuses an existing file |
lm describe connector-type KIND |
field reference for a connector kind: type, importance, doc, rules, and the default a key takes when omitted |
$ lm validate -f docs/apps/demo/entities/10-customer.yaml
✗ 10-customer.yaml — 4 error(s), 0 warning(s)
ERROR apiVersion
apiVersion is required
→ Use: apiVersion: connectors.lumnik.io/v1
ERROR kind
kind must be "Connector"
…
$ lm workflow disable acme-order-lifecycle
workflow acme-order-lifecycle enabled=false
$ lm app rm clients
Error: API error 400: cannot remove the last default app — apply a different app with default: true first, which demotes this one automatically
$ lm app rm lumilait
app lumilait deleted
Connectors, endpoints, sources
| Command | What it does |
|---|---|
lm connector list / get NAME |
the connector instances; one connector's flat stored fields |
lm connector apply -f FILE |
edit a connector from the file lm connector get prints — Editing a connector's stored fields |
lm connector delete NAME |
erases the connector's data: tables, run history, RAG chunks (erasure). Says what goes, then asks you to type delete. --dry-run stops there; --force skips the question. A table an entity reads blocks it (capture below) |
lm connector run NAME |
trigger an immediate run |
lm connector pause/resume NAME |
stop/restart scheduling; lm connector list shows a paused connector's cron followed by (paused), and lm connector run still runs it |
lm connector scope ls/set/add/rm/sync |
manage the connector's business scopes; sync re-stamps its already-indexed chunks |
lm endpoint enable/disable/reset-cursor C/E |
per-endpoint scheduler and cursor |
lm endpoint purge C/E |
delete one endpoint's rows after stating how many and asking you to type purge (--dry-run only counts, --force skips the question). For rest-generic endpoints and one-shot lm csv ingest tables; refused for jdbc-generic, csv-file and kafka — see Emptying one endpoint |
lm source rest test C |
pre-flight every enabled endpoint — the request a run would send, nothing written; non-zero exit if one fails |
lm source rest probe C --endpoint PATH |
sample one path with the connector's connection and credential; drafts the endpoint fragment to paste under spec.endpoints |
lm source rest from-openapi URL --name N |
generate a manifest (--scope repeatable, -o FILE, --apply upserts it) |
lm source jdbc test/discover/add |
open a JDBC source directly; the password is prompted without echo, or piped with --password-stdin. There is no --password |
lm source jdbc list-tables NAME |
the tables registered under a source the hub already holds |
lm source schema --scope TAG / lm source rediscover --scope TAG |
show / rebuild the scope's schema card (--scope is required) |
lm csv ingest FILE\|sftp://…\|s3://… |
one-shot CSV ingest into an ext.* table, no run recorded — flags on CSV & files. lm source csv ingest is the same command |
$ lm connector delete orders --dry-run
Would delete connector orders (csv-file) — 8 rows of connector.t_orders, its run history and RAG chunks (dry run, nothing deleted)
$ lm connector delete acme-desk-erp --dry-run
Error: API error 409: table connector.t_acme_desk_erp feeds entity acmecustomer — remove the entity first (lm entity delete acmecustomer)
A one-shot ingest:
$ 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`.
With the AI off, the askable line is replaced by: A one-shot: no run recorded, nothing indexed for chat.
Editing a connector's stored fields
lm connector get NAME prints the connector as the hub stores it. Edit that file and apply it back:
lm connector get erp-clients > c.yaml # keep the id: line, it names the connector to update
lm connector apply -f c.yaml
- Create a connector with
lm apply -fand itskind: Connectormanifest.lm connector applycreates onlycsv-fileandhello-world; for another type it says to uselm apply -f. - A file without
id:under a name already taken is refused, naming the id to add.
Data, runs & answers
| Command | What it does |
|---|---|
lm run list / lm run get ID |
connector runs, one row per endpoint (records in/out/skipped); list is newest first, --connector NAME, --limit N. An EVALUATED column appears once the tenant has an enabled Process: whether it has evaluated the rows the run wrote. get --wait-evaluated [--timeout D] [--interval D] waits for it, and exits 1 when the hub cannot say (no enabled Process, processes changed since the run started, or a run from before the hub recorded it) |
lm run cancel ID |
stop a run. A Running run stops at its next chunk; a Queued one ends at once. Refusals: Stopping a run |
lm dlq list / lm dlq get ID |
dead-letter entries (reason + offending row); --run RUN, --connector NAME, list --limit N |
lm dlq replayed ID --new-run-id RUN / lm dlq discard ID |
close an entry: carried again by that run, or dropped — the rules |
lm conflicts list |
fused-entity rows with a source conflict or a lookup ambiguity (--limit N, --entity NAME) |
lm observations list |
the workflow tape: watched documents, stillest first (--limit N); --workflow NAME narrows the hub's read to one workflow |
lm observations transitions |
the journal: each observed move, newest first, with the seconds spent in the state it left (IN_FROM_S, counted from the first observation for a document's first move); --workflow NAME, --doc KEY, --limit N |
lm observations stats |
the tape per workflow: watched, dormant, median detection delay (seconds; - when nothing is dormant) |
lm integration add NAME --url U --secret-stdin [--events T] |
register a webhook target. The signing secret is required and read on stdin. --events defaults to decision.*; --platform is a label only |
lm integration list / describe ID |
registered targets; the table shows scheme and host only, -o json\|yaml the full URL |
lm integration test ID [--event T] |
fire a catalog event at the target and report the delivery; decision.* events cannot be fired here |
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 (needs the deploy's SMTP settings — see the self-host guide) |
lm notify deliveries |
the mails the rules asked for, given-up ones first, with the kind of failure (--limit N) |
lm notify replay ID |
send one given-up mail again; prints where it stands |
lm outputs list |
what was sent out and whether it arrived, failures first (--limit N) |
lm outputs replay ID |
re-send one failed delivery as a new attempt |
lm data tables / lm data row HASH |
dynamic tables with your row count (unknown when it could not be counted); the raw row behind a citation, a timestamp as its UTC instant |
lm secret set/list/describe/rm |
encrypted secrets; values never returned. set NAME prompts without echo, set NAME - reads stdin, set NAME VALUE warns (shell history). --description, rm --force. rm does not check references: a connector naming the secret fails at its 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
884f487d orders default Completed 10 8 2 2 2026-10-19 01:38 2026-10-19 01:38
$ lm dlq list --run 884f487d
ID CONNECTOR RUN REASON ATTEMPTS MESSAGE
b6bc9601 orders 884f487d MalformedSource 1 /tmp/exports/orders.csv:9 — Record has 2 values but 5 column…
b4176d39 orders 884f487d MalformedSource 1 /tmp/exports/orders.csv:8 — Record has 6 values but only 5 c…
The ID columns are abbreviated — and the abbreviation works
lm run list and lm dlq list show the first 8 characters of each id. get, discard,
replayed and --run accept those 8 characters; an ambiguous prefix is refused. -o yaml
prints every id in full.
A full page says so
Lists answer in pages. Each takes --limit N, up to a cap:
| Command | Default | Cap |
|---|---|---|
lm run list |
50 | 200 |
lm dlq list |
100 | 200 |
lm inbox |
50 | 200 |
lm export runs NAME |
20 | 200 |
lm outputs list |
200 | 500 |
lm notify deliveries |
200 | 500 |
lm conflicts list |
200 | 1000 |
lm observations list / transitions |
200 | 1000 |
lm entity get NAME / lm entity ambiguous NAME |
100 | 1000 |
- A
--limitabove the cap is lowered to the cap.lm entity getrefuses it instead. - A full page prints
note: result hit the limit of N — there may be moreon stderr, so pipes and-o jsonstay clean. Raise--limitto see further back. --connectorfilters on the hub, so a quiet connector's old runs are still found.
Ask your data
lumnik Pro
lm ask, lm chat, lm rag and lm scopes need the AI. On lumnik Open, or with the AI off,
they say so: this hub's build has no Ask on Open, this hub has the AI switched off with the AI off.
| Command | What it does |
|---|---|
lm ask [--scope S] [--lang XX] [--previous "question"] "question" |
analytic text-to-SQL over the fused hub: the answer, a data as of line, any disputed rows, the rows the query returned and the SQL that ran (the guards). --lang sets the answer language (default: from LC_ALL, LC_MESSAGES or LANG). Without --scope, asks your default scope. --previous names the question this one continues: "Et SUP-004 ?" is answered as the full question, shown on an understood as line. -o json\|yaml for a script |
lm chat |
interactive REPL chat — in-REPL commands: :corpus NAME (or all), :scope TAG (or default), :topK N (default 10; the hub takes 1 to 50), :clear, :help, :exit |
lm scopes / lm scope / lm scope set TAG |
list scopes (a scope-bound user sees only granted ones); show/set your default (404 no user record on this hub…: see Tenants, users & scopes) |
lm rag search/status/reindex |
semantic search (--top-k N, 1 to 50, default 5); corpus status; re-index of the platform corpus (manifests, API, hooks) |
The rows under the answer
lm ask prints the rows its query returned under the answer, then how many there are, then the
SQL. When the hub withholds a phrased answer, these rows are the answer. Played on the
demo desk:
$ lm ask --scope desk "Quels clients GOLD sont à Lyon ? Donne leur code et leur nom."
Il y a 2 clients GOLD à Lyon :
- Code : P-1000, Nom : Hydraulique Lyon 1000
- Code : P-1008, Nom : Filtration Lyon 1008
Total de clients : 2
data as of: 2026-10-16T08:50:37.235585Z (last completed run)
disputed — a source disagrees on a value this answer filtered on:
P-1008 city = 'Lyon' counted t_acme_desk_erp: Lyon · t_acme_desk_crm: Villeurbanne
CODE NAME
P-1000 Hydraulique Lyon 1000
P-1008 Filtration Lyon 1008
2 rows returned
SQL: SELECT v.code, v.name
FROM connector."v_acmecustomer" v
WHERE v.segment = 'GOLD' AND v.city = 'Lyon'
LIMIT 500
try: lm conflicts list — where this scope has source conflicts or lookup ambiguities, row by row
- The query stops at 500 rows; every row returned is printed. A line break inside a value shows as
\n. no rows returnedwhen the query found nothing; no table when the answer ran no query.
For a script, ask for JSON (or -o yaml): one document, nothing else on stdout.
$ lm ask -o json --scope desk "Quels clients GOLD sont à Lyon ? Donne leur code et leur nom." | jq -r '.rows[] | .code + " " + .name'
P-1000 Hydraulique Lyon 1000
P-1008 Filtration Lyon 1008
| Field | Holds |
|---|---|
answer |
the sentence |
abstained |
true for a refusal. Test this, never the wording of answer |
sql, columns, rows, rowCount |
the query and what it returned; numbers stay numbers |
dataAsOf |
the last run that fed the scope |
understoodAs |
the full question, when --previous was used |
disputed, disputedTruncated |
the disputed rows, and whether the list was cut |
otherDoors |
the try: suggestions printed under the answer |
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'; city='Lyon' is refused |
lm view KEY -f FILE |
one record's detail (with the _conflicts ledger) |
lm view NAME [KEY] [--filter …] |
the same for a saved view of lm view list. A view named like a subcommand (list, get, apply, generate, rm) runs the subcommand instead |
lm view generate SCOPE "DESCRIPTION" [-o FILE] |
ask the AI to write a kind: View manifest over a scope; review it, then render with lm view -f |
lm view list [--scope S] |
the saved views you may read |
lm view get NAME [-o FILE] |
one saved view's manifest YAML — to stdout, or to a file |
lm view apply -f FILE / lm view rm NAME |
store a kind: View manifest; delete one (no prompt) |
lm app apply -f FILE / list / get NAME / rm NAME |
business apps — see Manifests |
lm workflow … |
lifecycles — see Manifests |
lm export apply -f FILE |
declare a kind: Export; inline credentials are refused, naming the field |
lm export list / lm export get NAME |
the exports your scopes can see; one export's manifest YAML (user:password@ printed as ***@) |
lm export run NAME [--wait] |
start a managed run. Without --wait it returns once the run is accepted; --wait follows it to Completed (prints the receipt) or Failed (exit 1). --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, 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 (latest by default) |
lm inbox [--limit N] [--all] [--process NAME] [--type TYPE] |
recent decision.* events — what needs you; --process/--type narrow the hub's read |
lm inbox ack ID |
dispose of a decision (idempotent) |
Admin & platform
| Command | What it does |
|---|---|
lm module list |
installed business modules, with ACTIVE/INACTIVE and the reason |
lm migration status |
migration status per schema: version, pending count, last applied (— when none). No apply: the hub migrates at every startup — capture |
lm hook list / lm hook get NAME |
the Kotlin platform hooks loaded at startup |
lm hook scripts |
the Kotlin scripts a connector manifest can name as script: hooks:<name>.kts: name, size, SHA-256 |
Search
| Command | What it does |
|---|---|
lm search apply -f FILE |
declare a kind: SearchIndex — validate, then create or update; -f - reads stdin (integrator) |
lm search rm NAME |
delete a declared index and queue the purge of its documents (Deleting); asks you to type delete (--force skips the question; in a script without it: aborted: no confirmation read) |
lm search query "TEXT" [--entity E] [--facet field=value]… [--limit N] [--cursor C] |
keyword search over the declared indexes, no model — the full guide |
lm search status [INDEX] |
every index (name, status, lag, documents, last rebuild), or one index's detail |
lm search rebuild INDEX [--wait] |
full rebuild; without --wait it prints the run id once claimed, --wait follows it to ready/failed/cancelled (--timeout, default 30m) |
lm search cancel INDEX |
request cancellation of an index's active rebuild; refused once it has reached publish |
The TUI
Bare lm (or lm ui) opens the TUI: The TUI.