Skip to content

How lumnik works

Everything you configure is a YAML manifest: apply it, then observe it land in the TUI. Every apply is checked first — refused naming the exact field, or upserted, idempotently. kind: Connector additionally has a dry-run, lm validate; the other kinds run the same checks at apply time.

This page is the apply-time story — what happens when you declare. The runtime story — what happens when a row changes and who gets warned — is How lumnik breathes.

The kubectl parallel

If you know kubectl, you already know most of lm: declarative objects, an idempotent apply -f, a dry-run check before you commit (lm validate — Connector manifests), a schema reference before you write a line of YAML (lm describe connector-type <kind>), and contexts to point the CLI at different hubs (lm config use-context).

That schema-reference command is connector-only, though: lm describe covers the five connector types, not the app kinds. For Entity, View, Process and Workflow the schema lives on each kind's own page (the Fields table) and in the TUI's :new screen, which opens a living, valid example of the kind you pick straight into $EDITOR — see CLI & TUI reference.

One place the parallel stops: lm apply never reconciles. It is a one-shot validate-then-upsert transaction, not a background loop.

Kubernetes controllers watch a cluster forever, pulling it back toward the declared state whenever something drifts. lumnik does not: apply it once, and it stays applied until you apply again. Nothing watches your source systems or the hub's own tables afterwards to "correct" drift.

The one declared exception is a Workflow's optional dormancy sweep: it periodically observes a document's state and alerts (decision.dormant.<workflow>) when it has sat too long — but it never rewrites the manifest or the data.

Six kinds, two families

Every manifest starts with kind: — six kinds, grouped into two families by what they build, not by a shared envelope: apiVersion is required and validated for Connector, declared-but-not-yet-enforced for the four app kinds (accepted today for forward compatibility), and simply absent from Entity's schema.

kind: apiVersion: What it declares Apply command Owning page
Connector connectors.lumnik.io/v1 — required, validated ingestion from one legacy source lm apply -f Connectors — Overview
Entity not part of this manifest's schema canonical fusion of N sources into one thing lm entity apply -f Entities
View apps.lumnik.io/v1 — accepted, ignored a read-only list a métier sees lm view apply -f Views
App apps.lumnik.io/v1 — accepted, ignored a métier app's own title, scope, icon and theme lm app apply -f The métier PWA
Process apps.lumnik.io/v1 — accepted, ignored a declared reaction to a row landing lm process apply -f Processes
Workflow apps.lumnik.io/v1 — accepted, ignored a declared document lifecycle over a status column lm workflow apply -f Workflows

Connector and Entity ingest and fuse; View, App, Process and Workflow build the app ladder on top of what's been fused. Note the one CLI irregularity: lm apply -f (no prefix) is Connector's own command — every other kind uses its own prefixed apply (lm entity apply, lm view apply, lm app apply, lm process apply, lm workflow apply), as the CLI reference documents.

Living with your objects

apply is only the write half — what you can list, fetch, or remove afterward differs by kind (verified against the lm CLI source, lm/internal/cli/):

Kind list get NAME delete NAME
Connector yes yes yes
View yes yes no
Workflow yes yes no
Process yes no no
Entity no no yes
App no no no

Entity has no list/get — lm entity apply and lm entity delete NAME are its CLI surface, and a fused entity is read back through GET /api/entities/{name}, not through lm. App has no list/get/delete at all — lm app apply is its only CLI surface, and the registry is read back through GET /api/apps (lm_user, scope-bound), which is how the PWA discovers its own identity at load. There is no single command that lists every kind at once — each shelf keeps its own list. See CLI & TUI reference for the full command tables.

One lifecycle

flowchart LR
  write["write the YAML"] --> validate["lm validate<br/>(dry run)"]
  validate --> apply["lm apply<br/>(idempotent)"]
  apply --> observe["observe in the TUI"]
  validate -. refused .-> write
  apply -. refused .-> write

Write, apply, observe — every kind, same shape (plus a lm validate dry-run for Connector manifests). A refusal at either check sends you back to the file, not into a retry loop.

Manifests are files in your repository — version them there, the same way you'd version a Kubernetes manifest. The hub stores the applied copy alongside its own state, and lm view get NAME / lm workflow get NAME re-extract that stored copy verbatim if your local file is ever lost.

The little languages inside the YAML

A manifest's fields are plain YAML, but several of them hold a small syntax of their own. Each one is documented in full on the page that owns it — this table is the map, not the manual.

Language One-line syntax note Where it shows up
JSONPath $ is the response root — $.data[-1:].id reads "the id of the last element of data" REST/GraphQL/SOAP pagination + promote fields — REST
cron 5 fields (UNIX crontab dialect), evaluated in UTC by the hub's own scheduler — no OS cron involved any connector's spec.schedule — Connectors Overview
glob * and ? only, in the final filename segment — no ** recursion CSV local/sftp/s3 transport.path — CSV & files
date patterns dd/MM/yyyy — capital MM is month, lowercase mm is minutes; add H/h/m/s only when the value carries a time parse-date — Transformers
regex full-match vs find semantics — differs per hook regex-extract / regex-replace / filter-where — Transformers
{{env:NAME}} / {{secret:NAME}} resolved from the hub's own process environment (OS env var, JVM system property as dev/test fallback) — not the lm secret registry; unresolved fails the run fast SOAP body/header templates — SOAP
${now} resolves once, to the ingestion instant enrich-context — Transformers
Kotlin .kts the escape hatch — full JVM trust, no sandbox/timeout/memory cap, classpath-only scripts mapping hooks (Transformers) and platform lifecycle/action hooks (developer SPI, documented beside the code)

Refusability as a feature

Every apply runs the same check that validate does first — a refusal names the exact field, not a stack trace. That is not a shortcoming to work around; it is the same posture the rest of lumnik carries: a refusal costs you a retry, a silent half-applied manifest would cost you a lot more. See Architecture — Declared over inferred for the why, and Connectors — Validate before apply for what a real refusal (and a real warning) look like on the wire.

See also