How lumnik breathes — from frozen rows to decisions
Ingestion answers "what is in my legacy system?". This page answers the other question — the one a métier team actually lives with: what happens when something changes, and what do I do about it?
The moment this page is about
A commande slips into EN LITIGE during the night batch. Nobody declared that status — it's fifteen years of habits. This morning, nobody knows. The client will call at 9:04, furious, and the chef des ventes will learn about the litige from him.
With a breathing hub, the same night goes differently: the row lands at 02:00, becomes an event in the same transaction, and a declared rule turns it into a decision seconds later. It is sitting in the Décisions tab before 02:01 — and because you wired your webhook on day one, the message that does buzz a phone (your Teams channel, your on-call SMS gateway) went out at 02:01 too. The client still calls at 9:04 — but this time the answer starts with "it was flagged at two in the morning — here's where it stands."
That is the respiration: your frozen system stops merely answering questions and starts warning you.
The chain, end to end
The story in one picture — for your integrator; the paragraphs above and below tell the same thing in words:
flowchart LR
erp[("legacy source")] -->|"your ingestion cron"| row["a new row lands<br/>(mirror table)"]
row -->|"same transaction"| ev["an event —<br/>new rows only"]
ev -->|"≤ 5 s"| proc["your declared rule<br/>when: statut=EN LITIGE"]
proc --> dec["decision.litige"]
dec --> inbox["Décisions tab<br/>lm inbox"]
dec -->|"≤ 5 s, retried"| hook["your webhook →<br/>the tool that pings phones"]
sweep["dormancy sweep<br/>every 15 min"] -->|"a document that STOPPED moving"| dec2["decision.dormant.*"]
dec2 --> inbox
dec2 --> hook
inbox -->|"✓ Traiter"| ack["acked — an event,<br/>timestamped, with who"]
Four mechanics behind the arrows:
- Only genuinely new rows emit. A re-ingested, unchanged row is event-silent by design (content-hash dedup) — the spine carries change, not noise. And events flow only once at least one subscription exists (a rule, a webhook): an unwatched table costs nothing.
- The respiration rides table-target ingestion — jdbc, CSV and kafka connectors, whose rows land in mirror tables. Rows ingested over REST/GraphQL/SOAP land in hybrid tables that are event-silent today: a rule on them never fires. Named on the product register, not hidden here — if your source is reachable only over REST, ask your integrator about this line first.
- The clocks, summed. The reaction consumers tick every 5 seconds; the distance
from row landed to decision in the inbox is seconds, and to your webhook seconds
more (3 delivery attempts in total on failure, with a journal you can read:
lm outputs list). The slow clock is your own ingestion cron — lumnik reacts at machine speed to what it ingests at the rhythm you chose. - The taxonomy is governed. Every rule emits
decision.<your-name>— the prefix is enforced at apply, so your inbox and your webhooks speak one vocabulary, and your eye can find your decisions in a mixed pile.
Where a human meets it — the channels
| Channel | What it does | State today |
|---|---|---|
| Décisions tab (PWA, phone & desktop) | the pile, newest first, badge on open; a treated decision stays visible, dimmed ✓ traité | pull — it waits for you to open it |
lm inbox / TUI inbox page |
the same pile for keyboard people (untreated only, by default) | same pull |
| Your webhook (n8n, Make, your own endpoint) | every subscribed event, delivered in seconds, retried, journaled, replayable — the road to the Teams message or SMS that actually buzzes a phone at 02:01 | the active channel. Registered by your admin (lm integration add, or the HTTP API), not retroactive: it forwards from registration onward |
Email (lm notify add) |
events matching a rule's pattern arrive as one mail per event, payload included — the channel for people who live in their mailbox, not in a tool | live once the deploy sets the SMTP knobs (Email notifications); mocked until then, and the boot log states the posture. Best-effort: a failed send is logged and skipped — the webhook is the channel with retries and a journal |
One model to know before you plan on it: the inbox is tenant-wide. Everyone with app access sees every decision; there is no per-métier fence on this surface, no assignment, no escalation, no out-of-office. "Who handles it while she's on congés?" answers itself — everyone still sees it — but "only the ADV sees ADV alerts" is not this surface's promise.
The gesture: what ✓ Traiter is, and is not
The click clears it from everyone's untreated pile and marks, for the whole team, that
you took it — that is what it does for you. Under the hood it is an event
(annotation.decision.acked, timestamped, carrying who), which is exactly what makes it
proof rather than a checkbox. It is not a write to your source system: lumnik is a read-only
mirror by doctrine (Reversibility), so the correction itself still
happens in the legacy screen, by the person who owns it.
That division of labour is the point, not a limitation: lumnik removes the hunt, not the
correction. The ADV's five morning filters, declared once as kind: Process rules by
your integrator, become the pile; her morning stops being export → filters → copy order
numbers into a mail and becomes open the pile → decide → correct in the ERP → ✓.
Silence also emits
Rules catch what happens. The dormancy sweep
catches what stopped happening: a declared workflow watches every document, and one that
sits non-terminal past its threshold becomes decision.dormant.<workflow> — the 80 k€
devis nobody relanced surfaces by itself, named, with immobile depuis le….
Two details to plan around: the threshold is measured from first observation — the watch starts counting when you deploy it, so a before/after story needs the watch running from month one. And dirty statuses don't escape it: the apply's data-truth report names every value your manifest missed, with its row count — and even an undeclared state is watchable, counted, never hidden.
What it removes — the DAF's list
- The morning anomaly hunt: the hand-filtered export becomes decisions pushed to the pile.
- The status call: "où en est la commande Durand?" is the fiche on the phone, and the chat for the odd question — with refusal instead of invention when the data can't answer.
- The watching by eye: dormant documents surface themselves.
- The accidental discovery of systems disagreeing: the fiche's
_conflictspanel shows which source said what.
And the two things it deliberately does not remove: the correction in the ERP (yours, by design), and the judgment call on each decision (the pile is ordered by time, not by severity — your eye ranks it).
What remains, three months later
The inbox shows what retention keeps: events — decisions and acks — live 30 days by default, then the purge takes them. Three memories outlive that window:
- the observation ledger: every watched document, timestamped — first seen, in-state
since, alerted when — kept deliberately.
lm observations listshows the tape;lm observations statsaggregates it per workflow: watched documents, currently dormant, median detection delay — the "detected at J+N" figure, computed; - your webhook target: forward
decision.*andannotation.*into the tool where your team already keeps proof, and the trail is yours, on your retention; - the mirror itself, of course — the data keeps its history of hashes and
_ingested_at.
Deployment guidance, not a footnote: wire the webhook on day one
If an auditor will ever ask "prove this alert was handled" — or a phone must ever buzz at 02:01 — register the webhook the day you deploy. It forwards from that day onward, never retroactively.
Named limits, in one place
- No push to métier surfaces — the tab and the CLI are pull; the webhook is the road to tools that push. (An event stream does exist for integrators, on the platform API.)
- The inbox is tenant-wide, time-ordered, without assignment or severity.
- ✓ Traiter never writes to the source — by doctrine.
- Webhooks forward from registration onward, never retroactively.
- REST/GraphQL/SOAP-ingested tables are event-silent today — the respiration rides jdbc/CSV/kafka ingestion (on the register).
- Declared status aliases (encours/EC…) serve the dormancy watch and the chat's background knowledge, not yet the chat's SQL or view filters — the chat matches the literal values sitting in the column, so mixed spellings need naming or cleaning.
- Email is best-effort — a failed send is logged and skipped, no retry, no journal; delivery stays mocked until the deploy sets the SMTP knobs. The webhook is the guaranteed-effort channel.
- No usage/adoption dashboard — operational metrics (
/q/metrics) are the only proxy, per endpoint, if your ops scrape them.
See also
Outputs — the roads out — the wiring manual for every channel on this page · Processes — react · Inbox — decide · Workflows — declare · The métier PWA · The WOW, step by step — Act 2 runs this whole chain on the demo dataset, EN LITIGE included.