Admin tasks — lm and the API behind it
Day-to-day administration of a running hub: webhooks, deliveries, runs, secrets, migrations,
modules, hooks, tenants. Every task below starts from an lm command or a TUI screen
(lm ui); raw HTTP appears only where no lm command exists.
No admin web console ships with lumnik.
Who can call what
Roles are matched literally, with no hierarchy: lm_superadmin alone does not open an
lm_admin door, and the reverse.
| Task | Gesture | Roles needed |
|---|---|---|
| Wire webhooks | lm integration · :integrations |
lm_integrator + lm_admin, no scope:* role (or lm_admin alone over /api/tenants/{tid}/integrations) |
| Read and replay deliveries | lm outputs · :outputs |
lm_integrator + lm_admin (or lm_superadmin), no scope:* role |
| Runs and dead letters | lm run, lm dlq · :runs, :dlq |
lm_integrator |
| Read secrets (metadata) | lm secret list, describe |
lm_integrator |
| Create, rotate, delete secrets | lm secret set, rm |
lm_integrator + lm_admin |
| Migrations, modules, hooks list, event catalog | lm migration, lm module, lm hook list, GET /api/platform/events |
lm_integrator |
| Declared workflows | lm workflow |
lm_integrator |
| Live event stream | the TUI, or GET /api/platform/events/stream |
lm_integrator, no scope:* role |
| Toggle a hook for a tenant, read one hook | POST /api/platform/hooks/{id}/toggle, lm hook get |
lm_integrator + lm_admin |
| Tenants | /api/tenants |
lm_superadmin |
| Orgs, users, tenant roles | /api/tenants/{tid}/… |
lm_admin, own tenant only |
A scope-bound integrator sees only the runs and dead letters of connectors whose scopes it holds (Scopes).
No lm command prints your roles. Ask the hub (needs lm_user):
$ curl -s http://localhost:8080/api/bff/v1/me -H "Authorization: Bearer $TOKEN" | jq '{tenant: .tenant.id, roles}'
{
"tenant": 1,
"roles": [
"lm_integrator",
"lm_admin",
"lm_user"
]
}
tenant is the {tid} the /api/tenants/{tid}/… paths want. Getting a token for curl:
Test the API.
Send events out to another system
Register, test and remove a webhook target with lm integration: Outputs — wire a webhook.
In lm ui, :integrations: n registers, T tests, Del removes, d shows recent deliveries.
Settings lm does not expose
lm integration add always registers an enabled target in HMAC mode. For another mode, or to
change a registration, use the HTTP door /api/platform/integrations:
| Field | Values | Default |
|---|---|---|
name |
unique within the tenant | required |
targetUrl |
the receiver's URL | required |
eventTypes |
exact types or .* prefixes |
required |
authMode |
HMAC (X-Lumnik-Signature: sha256=…), BEARER (Authorization: Bearer <secret>), NONE |
HMAC |
secret |
the HMAC key or the bearer token; never returned (hasSecret: true) |
none |
platform |
generic, make, zapier, n8n — a label only (lm integration add --platform) |
generic |
enabled |
true / false |
true |
Rotate a target's secret (a PUT applies only the fields present; a blank secret is ignored):
$ curl -s -X PUT http://localhost:8080/api/platform/integrations/1001 \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"secret":"the-new-shared-secret"}' | jq
{
"id": 1001,
"name": "ops-hook",
"targetUrl": "http://localhost:18955/hook",
"platform": "generic",
"authMode": "HMAC",
"eventTypes": [
"connector.run.*"
],
"enabled": true,
"hasSecret": true
}
Change the receiver's copy in the same window: the next delivery is signed with the new secret.
Watch deliveries and replay a failure
$ lm outputs list
ID INTEGRATION EVENT STATUS WHEN ERROR
1006 ops-hook connector.run.completed no response 2026-10-19 01:46 connection failed (ConnectException)
1005 ops-hook connector.run.started no response 2026-10-19 01:46 connection failed (ConnectException)
1001 (deleted) connector.run.completed no response 2026-10-19 01:44 endpoint gone, unsubscribed, or entity_filter no longer matches — retries stopped
1000 (deleted) connector.run.started no response 2026-10-19 01:44 endpoint gone, unsubscribed, or entity_filter no longer matches — retries stopped
1003 ops-hook connector.run.completed ok 2026-10-19 01:45
1002 ops-hook connector.run.started ok 2026-10-19 01:45
1004 ops-hook connector.run.completed ok 2026-10-19 01:44
Failures come first. A failed delivery is retried automatically, 3 attempts in all, about 30 s apart. After the third, replay it once the receiver is back:
$ lm outputs replay 1006
replayed: delivery 1006 — a new attempt is on record
$ lm outputs list
ID INTEGRATION EVENT STATUS WHEN ERROR
1006 ops-hook connector.run.completed no response 2026-10-19 01:46 connection failed (ConnectException)
…
1007 ops-hook connector.run.completed ok 2026-10-19 01:46
The replay is a new row (1007); the original row stays as it was, still listed as failed.
In lm ui, :outputs shows the same list; ^R replays the selected delivery.
ERROR column |
Meaning | Gesture |
|---|---|---|
connection failed (ConnectException) |
the receiver did not answer | bring it back, then lm outputs replay ID |
endpoint gone, unsubscribed, or entity_filter no longer matches — retries stopped |
the target was removed, disabled, or no longer subscribes to that event | re-enable or re-subscribe, then replay; a removed target accepts no replay (404) |
Find the event types you can subscribe to
No lm command lists the catalog; the running hub serves it:
$ curl -s http://localhost:8080/api/platform/events -H "Authorization: Bearer $TOKEN" | jq -r '.[].type'
org.created
org.updated
org.moved
org.archived
connector.run.started
connector.run.completed
connector.run.partial
connector.run.cancelled
connector.run.failed
platform.secret.created
platform.secret.updated
platform.secret.deleted
platform.secret.read
data.row.changed
annotation.decision.acked
Each entry also carries its module, a description and the payload fields. decision.* types
are not in this list: each kind:Process and kind:Workflow names its own, and a decision.*
subscription matches them when they fire. What each type means:
the event registry.
Watch ingestion: runs, dead letters, live events
Runs and dead letters, step by step: Diagnosing a failed run.
In lm ui, :runs, :dlq, :sources and :endpoints update by themselves while runs happen.
They follow the live event stream, which your own tooling can read too:
| Door | GET /api/platform/events/stream?topic=connector.run.* (Accept: text/event-stream) |
topic |
repeatable; * is a wildcard over the event type; none = every event |
| Each element | {"topic": "<type>", "payload": "<JSON as a string>", "capturedAt": "<ISO-8601>"} |
| Order | an event shows once every older database transaction has ended: none is missed, and one left open delays them all (Outputs) |
| Scope-bound caller | refused (scope not granted); in lm ui the views then refresh only when opened |
Keep a connector credential: the secret store
Connector manifests name a secret (*_env fields); lm secret stores its value, encrypted.
No command returns a value.
$ lm secret set ERP_DB_PASSWORD --description "ERP read account"
Value for ERP_DB_PASSWORD:
Created secret "ERP_DB_PASSWORD"
$ lm secret list
NAME ROTATED_AT DESCRIPTION
ERP_DB_PASSWORD - ERP read account
$ lm secret rm ERP_DB_PASSWORD
Type 'delete' to confirm removal of "ERP_DB_PASSWORD": delete
Deleted secret "ERP_DB_PASSWORD"
- The value is typed at the prompt and not shown. From a script:
lm secret set NAME -reads stdin. seton an existing name rotates the value (Updated secret "…",ROTATED_ATfilled). Without--description, the description stays;--description ""clears it.lm secret describe NAMEshows metadata only;lm secret rm --forceskips the prompt.- Which connectors read the store, and which read the hub's environment instead: REST, CSV, JDBC.
Trace a citation back to its source row
lm data row <HASH>
Takes the row_hash of a chat citation and prints the raw source row (all values as text).
It answers 404 when the hash resolves to nothing in your tenant. A scope-bound caller reads the
row only if every connector writing its table carries one of its scopes.
A citation names a content, not a row. Two rows of a REST table with the same content (different
external_id) share one chunk and one hash: the command prints the oldest of them.
Check migration drift
$ lm migration status
SCHEMA VERSION PENDING LAST APPLIED
core 75 0 2026-10-19 01:36
identity 18 0 2026-10-19 01:36
rag 6 0 2026-10-19 01:36
Migrations apply when the hub starts; there is nothing to run by hand. PENDING 0 on every schema
means the database matches this hub build. After an upgrade: Upgrade.
See the installed modules
$ lm module list
NAME VERSION SCHEMA STATUS REASON
core 0.1.0 Platform ACTIVE
identity 0.1.0 Identity & Tenancy ACTIVE
llm 0.1.0 Model provider INACTIVE disabled:lumnik.ai.enabled
rag 0.1.0 Platform RAG INACTIVE disabled:lumnik.ai.enabled
This hub is a Pro build with the AI switched off (Switching the AI off).
An Open build lists no llm or rag line.
Turn a hook on or off, and read declared workflows
| Gesture | What it does |
|---|---|
lm hook list · lm hook get NAME |
the hooks this hub loaded |
lm workflow list · lm workflow get NAME |
the tenant's declared kind:Workflow manifests (Workflows) |
POST /api/platform/hooks/{hookId}/toggle with {"enabled": false} |
turns one hook on or off for your own tenant (a tenantId= naming another one is 403). The hub you called applies it at once, another replica within 60 s |
Manage tenants, orgs and users
No lm command manages identity; use the HTTP doors. Deletes archive, never erase. The gestures
in context (onboarding, offboarding): Identity administration.
The doors, under /api/tenants and /api/tenants/{tid}/…: tenants (lm_superadmin), orgs,
users and tenant roles (lm_admin, your own tenant). Their full contract is in your hub's
/q/openapi (Test the API).
Org changes emit org.created, org.updated, org.moved, org.archived. lumnik stores no
passwords: users sign in through Keycloak or your IdP. On Pro, a user's hub record is created at
first login.
Act on behalf of a tenant (impersonation)
- Impersonation is a JWT claim minted by your IdP:
acting_as_tenant: <tid>, on a token that carrieslm_superadmin. There is no hub endpoint that mints it. - It switches the tenant, never the roles: the roles in the token are the ones checked.
- A token carrying the claim without
lm_superadminis refused:403 {"error": "lm_superadmin required to impersonate a tenant"}.
Harden the deployment
| Concern | What the hub does | What you do |
|---|---|---|
| Secrets | encrypted at rest, never returned by any door | protect the .env that holds the keys; key rotation is not supported (RLS & credentials) |
| Impersonation | honoured only for lm_superadmin |
grant lm_superadmin to as few accounts as possible; an impersonated write is audited in the target tenant under the superadmin's user id, with nothing marking it as impersonation; the impersonation itself shows only in log lines |
| Dev bypass | grants every role to every caller | never set lumnik.security.dev-bypass=true outside development |
| Platform doors | /api/platform/* requires lm_integrator |
restrict /api/platform/* and /q/* at your proxy if the hub faces the internet |
Troubleshooting
| Symptom | Cause | Gesture |
|---|---|---|
401 with WWW-Authenticate: Bearer on /api/platform/* |
the token expired or was not sent | lm login again |
API error 403: missing role lm_integrator |
the token lacks lm_integrator |
read your roles (above), then have it granted |
API error 403 (forbidden): no details from the server — your account may lack the required role (raw HTTP: 403, empty body) |
a role the door needs is missing — often lm_admin |
read your roles, then have it granted |
access refused (403): scope not granted on lm integration, lm outputs (raw HTTP on the event stream: 403 {"error":"scope not granted"}) |
your account holds a scope:* role; these doors are tenant-wide |
use an account with no scope:* role |
403 on /api/tenants/{tid}/… while you hold lm_admin |
{tid} is not your token's tenant |
use tenant from /api/bff/v1/me |
400 {"error": "Unknown event type: …"} on lm integration test --event … |
the type is not in the catalog — decision.* never is |
test with a catalog type; verify decision.* with a real decision |
API error 400: HMAC mode needs a secret … on lm integration add |
no --secret-stdin |
pass the secret on stdin — see Send events out |
| A failed delivery is no longer retried | it used its 3 attempts | lm outputs replay ID |
lm outputs replay ID answers 404 for an id in your list |
the target was removed with lm integration rm |
register the target again; new events reach it |
If none of that is it
- Your hub serves its live API contract at
/q/openapiand a try-it console at/q/swagger-ui/: Test the API. - Security reports:
contact@lumnik.fr(acknowledged within 72 hours). It is not a support channel.