Repository navigation
Releases
v1.11.2
v1.11.2 — 2026-11-10
The open edition now has a public image. Each release tag builds the hub and pushes it to
ghcr.io/icreated/lumnik (tags X.Y.Z, latest, main) and to Docker Hub as icreated/lumnik.
./deploy/selfhost/up.sh pulls it: no more Maven inside Docker before a first run. --build still
works. No code change, no migration, lm stays at 1.8.0.
v1.11.1
v1.11.0
v1.11.0 — 2026-11-09
A Process can now run on REST rows and on a fused Entity; mails are stored, retried and replayable;
event patterns are strict; the csv reader tells you when it guessed the encoding. Four migrations
(V87 to V90), all new tables or indexes in core, run by themselves at start. lm 1.8.0.
Before you upgrade
- Migrations V87 to V90 add tables and one index. Nothing existing is altered. They hold the paged
work of a lookup fan-out (V87), the dead letters of outbox consumers (V88), the stored mail
deliveries (V89) and one index on that last table (V90, a plainCREATE INDEXon a new table:
the build is short). No action. The next free number is V91. - A rest-generic run that ended
Completedwith zero rows can now endFailed. A
pagination.response_paththat reads nothing on the first page fails withpagination.response_path reads nothing on the first page — check it against the source's answer; one that reads an object
fails withpagination.response_path must read a list, not an object. An empty list still
succeeds.lm source rest test(also GraphQL and SOAP endpoints) now says the same instead of
200 0 ok: a test that was green may turn red, and the run would have failed too. - Decimal columns are plain
NUMERIC, no longerNUMERIC(18,4). A fifth decimal used to be
rounded in silence; now it is kept. An existing table is widened by the next write that needs it.
Whole values read1024944, not1024944.0000. Check any view or query that counted on four
decimals. parse-datewith ayyformat needscentury-from.98was read as 2098 without a word.
Setcentury-fromto the first year of your 100-year window (1950 covers 1950 to 2049).
lm validaterefuses the hook without it, and refusescentury-fromwhen no format readsyy.- Event patterns are strict (mail rules, webhooks). A
*is accepted only as a final.*, and
prefix.*needs the dot:decision.cold.*no longer matchesdecision.coldroom-x. Create and
update answer400withevent pattern '<p>' is not accepted: '*' is only allowed as a final '.*'.Rules already stored with a stray*stay as they are and never match. List your rules and
webhooks and fix them. - A mail rule with an unknown template is refused at creation (
lm notify add,
POST /api/notifications), naming the shipped templates (event-notification,
decision-alert). Before, every mail of such a rule was given up at render. - Mails are stored, one delivery per rule and event, and retried up to 3 times by a 30-second
sweep. They are at-least-once: a delivery whose outcome could not be written is sent again once its
lease runs out. A rule onnotification.*never receives the newnotification.mail.given-up
event (loop guard): carry it with a webhook. - A decision mail is subjected by the decision's sentence (the
messagelm inboxshows, cut at
100 characters). Other mails keeplumnik event: <type>. - A Process
when:that compares to a{x}literal now means the columnx. Apply refuses it if
xis not a column, and a condition already stored that way no longer matches. Re-read your
=conditions for braces. - A Process on an Entity refuses a decimal join key, and a date joined to a timestamp. These keys
cannot be indexed under row-level security. Key the Entity on text, whole-number or timestamp
columns. - A Workflow cannot use
spec.check.trigger: ingestionon a REST table. Apply refuses it with a
sentence saying why. A Workflow can still watch a REST table. - A catch-all
data.row.changedsubscription (webhook or mail rule without an entity filter) now
sees rows changed by REST connectors. - A REST table gets a
(tenant_id, row_hash)index on its next write. The build is not
concurrent: writes to that table wait while it runs. Upgrade off-hours if you hold big REST tables. - A changed
mapping:on a csv-file connector drops its cursor. The next run reads the files
again. An identical re-apply keeps the cursor.lm applyalso warns when aparse-date,
parse-numberorparse-booleantypes a column the table holds as another type: the table is not
retyped; delete the connector, apply, run. - A csv-file with no
parser.encodingand bytes above 0x7F now ends the runPartial. The run's
error:says the encoding was guessed, and asks you to declareparser.encoding. Declare it, then
lm endpoint reset-cursor. Only the first 8 KB are examined. - A byte the declared encoding has no character for stops the read with a dead letter naming the
encoding, the byte and the line. Rows before it are read; rows after it are not. Set the right
parser.encoding, thenlm endpoint reset-cursor. - A csv-file header is trimmed. A header padded with spaces (a Firebird
isqlexport) gives the
bare name; values keep their padding. - A NUL byte in a text value dead-letters that one row. Before, PostgreSQL refused it, the whole
chunk failed and the run endedFailed. Addclean: { remove-control-chars: true }to keep the row. - A remote csv file (sftp or s3) is named after its stem, as an upload is:
clients.csvlands in
ext.clients, notext.clients_csv. A table already created asext.<name>_csvkeeps its name,
and a new ingest of the same file createsext.<name>. Check queries and Entities that read the old
name. - A cron slot outside the schedule
windowis skipped, not run late at the first in-window tick:
0 6 * * *with window22:00-06:00used to run at 22:00. A slot inside the window missed while
the hub was down still catches up. - An Entity read filter with a date-only operand is that day's midnight in the Entity's
zone:
(UTC without one).lm entity get --filter due=2026-07-01can return other rows on an Entity with a
zone. A date-time operand is still an instant. - Entity HMAC secret (
LUMNIK_ENTITY_RESOLUTION_HMAC_SECRET): keep it stable for the life of the
data. Changing it in place is not supported. After a change,lm entity resolveleaves the rows an
exactrule resolved ambiguous, with no entity id; a pair declared withlm entity assert-same
keeps its link. If it changed by accident, put the old secret back before the nextlm entity resolve: after a resolve ran, nothing brings the links back. No rekey command exists yet. Open view
page links also stop working. metadata.name is requiredno longer says(kebab-case): nothing enforced that case. To take
over a one-shot csv ingest, use the namelm connector listshows.
Added
- A REST connector's rows can feed a Process and a Workflow. Write a
kind: Processor
kind: Workflowon anext.*table, once it exists (first ingest). It fires on new rows and on rows
whose source content changed; rows that existed before the apply are not evaluated. - A Process can key its decision by a column:
spec.emit.key: <column>.lm inboxthen shows the
document (analysisrequest-24) instead of a row hash; a null or blank value falls back to the
hash. An unknown column is refused at apply. - A Process
when:can compare two columns of one row:"measured_at>{calib_valid_until}", quoted
in a YAML list. Operators=,>,>=,<,<=; both columns of one kind; a null never
matches. - A Process can listen on a
match:Entity: setspec.on.tableto the Entity's view. The rule is
evaluated on the fused row; one decision per source event and canonical key. Entities with
resolution:are refused; rows already there at apply are not evaluated. A change in a lookup table
(joins:) re-evaluates every fused row it reaches: durable, paged, resumes after a crash. The hub
builds the indexes these reads need, and warns while one is still building. lm process applyandlm entity applyprint each hub warning on stderr aswarning: …. The exit
code stays 0.- Dead letters for outbox consumers. A consumer that fails on an event no longer loses it.
GET /api/platform/events/dead-letters(optionalconsumer=,open=true; the 200 most recent)
lists them;POST /api/platform/events/dead-letters/{id}/replayredoes one (Process, webhook and mail
letters). A failed replay answers500and leaves the letter open. Nolmcommand yet. - Mail deliveries are visible and replayable.
lm notify deliveries [--limit N]lists them, given-up
first: status, attempts, when, kind of failure; never subject or body.lm notify replay IDsends a
given-up mail again. A sent mail answers 409, another tenant's 404. HTTP:
GET /api/platform/notification-deliveriesandPOST …/{id}/replay, same roles as the webhook door. - A given-up mail is told by an event:
notification.mail.given-up(delivery_id,rule_id,
event_type,failure; never the mail or the address), emitted once. Carry it with a webhook. A
failure names its kind:authentication refused (SMTP 535),recipient refused (SMTP 550),
refused by the relay (SMTP n),relay unreachable,TLS refused. - Several hub nodes follow each other's Entity changes. A node checks every 3 s
(lumnik.entity.registry.refresh-every) and reloads only when the stored Entities changed. - An Entity can declare
zone: Europe/Paris. A date is its midnight in that zone, for alignment,
the ledger's day comparison, join keys and search marks. Absent means UTC: existing Entities do not
move. A region name; an offset is refused. - REST:
transform: from_odata_datereads/Date(<ms>)/and/Date(<ms>±<minutes>)/(OData v2) as a
UTC timestamp. Pagination presetsodata-v2andodata-v4: the count arrives only if the endpoint
path:asks for it ($format=json&$inlinecount=allpages,$count=true).pagination.total_header
reads a range total such asitems 0-19/57(a*total is refused).
`http_version: "1.1...
v1.10.3
REST connectors refuse to report a partial read as complete. lm describe connector-type now tells you
what each key requires and what it defaults to. No migration.
Before you upgrade
- A rest-generic run that used to end
Completedcan now endFailed. Each case lost rows in
silence. Runlm validateon your connectors, then watch the first runs.- The source answers the same page twice in a row. The run stops with
the source answered the same page twice in a row — the paging parameter is not reaching it (check offset_param, page_param or the preset). - The read reaches the 10,000-page cap with pages left:
reached the page cap (10000 pages) before the source's last page — refusing to report a partial extract as a complete one. - The list's declared total moves during the read. It is read again once from page 1; if the total
moves again the run stops, naming both totals. This now covers the presetsnestjs,spring-page,
spring-paged-model,micronaut,fastapi,spring-hateoasanddrf-page.
- The source answers the same page twice in a row. The run stops with
- Manifests refused at
lm validatethat passed before.- A
path:query that names the pagination's own parameter (path: /items?size=500with
kind: fastapi). Before, the parameter was sent twice. - A
promoteentry with a reserved column name (id,data,row_hash, …), a duplicate name, or a
JSONPath that does not compile. Before, the run died at its first chunk. - A blank value where a rule needs one (for example
watermark_column: " "withmode: watermark). - An invalid
default_rate_limitis no longer ignored after the first use.
- A
- Wording moved. A script that reads these must follow: the
lm run getcell-error line now reads
N cell(s) not delivered as declared (…) — the rows were still written; a bearer auth missing its
token is reported atspec.auth; index errors carryspec.target.indexes[n].columns. - Stored values. A CSV header named
row_hashstays indataand no longer becomes a column. An S3
export prefix made only of spaces publishes at the bucket root.
Added
- rest-generic:
auth: { kind: session }logs in with a form (Spring Security_csrfand Tomcat
j_security_checkshapes), holds the cookie per connector and tenant, and logs in once more and
replays when it expires.response_format: xmlreads an XML answer. Attributes sit under@, text under#text; a DTD
and external entities are refused.- The preset
prestashop. - On the
pagestrategy,total_pathwithsize_paramends the read once the rows read reach the
total, so a Micronaut 4 list no longer asks for one empty page more.
- Cell hook
parse-boolean(0/1,O/N,Oui/Non,true/false,yes/no,vrai/faux,t/f,
or your owntrue:/false:lists) gives a real boolean column. lm conflicts list --entity NAME(andGET /api/entities/conflicts?entity=NAME). An unknown
entity is404; an entity outside your scope is the uniform403.lm describe connector-typeprints the rules of each key (required when,ignored unless,
one of,at least one of, element shapes) anddefault:.-o jsonkeepsdefault.
Fixed
- A JDBC source no longer keeps idle sessions: a pool closes its idle connections after about 30
seconds, and a pool nobody asked for in 10 minutes is closed. The first read after a pause pays one
connect. A rotated source password is followed. A slow source no longer stalls other connectors. - A deleted connector drops its OAuth2 token and session cookie at once; both are dropped after an
hour idle. - A whitespace-only cell stays empty under the
parse-*hooks and is no longer counted in
cell_errors. lm source rest probedraftsid_2instead of a reserved promote name.regex-replaceand the constant patterns of CSV inference compile once.- Process and Workflow manifests accept a
spec.scopewith spaces around it, stored stripped, as the
other kinds already did.
v1.10.2
v1.10.1
v1.10.1 — 2026-10-25
Fixes for a first install and for clearer messages. No migration, nothing to do before upgrading:
status codes and JSON shapes are unchanged, only some English wording.
Fixed
deploy/selfhost/up.shno longer writes a fresh.envover an earlier install's volumes.
The volumes keep the passwords of the.envthat created them; new secrets used to leave Keycloak's
admin and the realm accounts locked. It now stops, writes nothing, names the volumes, and prints
two ways out: put back the old.env, ordocker compose -p <project> … down -v, which deletes
the data. If Docker cannot list the volumes, it stops too.lm ask,lm chat,lm rag searchandlm scopessay why Ask is not there. On this edition:
this hub's build has no Ask: lumnik Open has no chat, no lm ask and no semantic search (lm module list shows no rag module); Ask ships in lumnik Pro. Before, they printed
API error 404: Unable to find matching target resource method.- The no-scopes warning says what is lost on this edition:
no scopes declared — a user bound to a scope will not see this connector, its runs or its dead letters. - English text says "scope", "business app" or "business user", not the French "métier": hub
messages,lmand TUI help, docs. Twokind:Workflowapply refusals now read
belongs to another scopeandin a scope you do not hold. - The v1.10.0 notes said a mapping naming "a column the file does not hold" is refused. It is refused
only when the columns are declared (a headerless file withcolumns:, orparser.lines: true).
v1.10.0
v1.10.0 — 2026-10-25
A manifest that would fail a run is now refused when you validate it: a hook that does not exist, a
parameter it does not read, a column outside the declared ones, a wrong shape. Events are no longer
missed when database transactions end out of order. Processes say when they have read a run.
Workflows alert on due dates. New hooks read EDIFACT, fixed-width files and Windows mojibake.
Before you upgrade
- Take a backup. This release carries ten migrations (V76–V85). Migrations are forward-only.
Seedocs/operations/upgrade.md. - Removed secrets are deleted for good. V84 deletes every secret removed with
lm secret rm
before this release, in every tenant. The value is gone, not hidden. This cannot be undone. - Plan for short locks at first boot.
- V85 builds two indexes on
core.event_outbox. Writes to the outbox wait while they build. - V78 checks every dead letter once under a table lock.
- Time both on a copy if those tables are large.
- V85 builds two indexes on
- Run
lm validateon every manifest. Each of these used to pass and now is refused:- in a csv-file mapping: an unknown hook, a parameter the hook does not read
(upper hook: unknown parameter 'colums' — accepted: columns), parameters that are not a map,
a tier that is not a list, an unknown key underspec.mapping, a column that is not declared
(a headerless file withcolumns:, orparser.lines: true; a header read at run time is not checked),
a value outside a knob's allowed values, tworegex-extractwriting the same column; - a boolean written
yes,no,onoroff: writetrueorfalse; - a
mapping:block onjdbc-genericorrest-generic: those rows land as the source holds
them, remove the block; - in rest-generic
spec.defaults:base_url,auth,default_rate_limit,graphql_pathor a
key that switches the request shape; - a GraphQL
incremental.paramthe query does not declare; - a Kotlin
script:that is notclasspath:orhooks:, andcolumn:on akotlinhook; - an unknown SOAP
{{placeholder}}, an unknownon-errormode.
- in a csv-file mapping: an unknown hook, a parameter the hook does not read
- Fix scripts that parse
lmoutput.lm apply -f,lm process applyandlm workflow applyprint one line, not JSON.lm -o yamluses the JSON key names (recordsIn,cellErrors).lm inboxprints[acked]and English ages.
- Fix scripts that close dead letters.
lm dlq replayedneeds--new-run-id: the run that
carried the row. An entry no run carried is closed withlm dlq discard. PastREPLAYEDentries
with no run becomeDISCARDED. - Fix scripts that call the API by hand.
- The notification, integration and tenant-user doors refuse an unknown JSON key with
400.
Write keys in camelCase (entityFilter, notentity_filter). GET /api/entities/{name}answers400for a filter on a column it cannot filter. Before, the
filter was ignored and every row came back.- A
409from the identity and secret doors now carries{"error": …}. Before, it was empty.
- The notification, integration and tenant-user doors refuse an unknown JSON key with
- Helm: authentication is on by default.
auth.modeisoidc.dev-bypassrenders only with
auth.iUnderstandNoAuth=true. - A REST header
value_envthat resolves to nothing stops the run. It readslm secretfirst,
then the environment. The run stops withHeader X unavailable (env=Y). Before, the header was
left out.
After you upgrade
- Columns declared
on-error: dlqorfailnow act. Before, the mode was accepted and ignored. lookup on-missing: null(unquoted) now empties the cell, as documented. Before, it kept the
value.- Kotlin
contextvalues keep their YAML types. A script that casts a numberas Stringfails. - rest-generic incremental runs look back. The next request starts before the mark, by the
last run's length plus 60 s. Rows near the mark are read again and upserted.
incremental.lookback: 0srestores the old behaviour. - GraphQL incremental runs now read only what changed. The mark goes in the query variables.
Before, it went in the URL, and every run read everything. - Entity columns fed by numbers become numbers. All whole numbers fuse as
bigint, any decimal
asnumeric. Before, both were text. Re-apply the entity. The entity JSON still sends strings. - Archive names carry the file's time:
deliveries.20261022T163544Z.csv. Update anything that
looked for the old names. - The default dormancy alert is in English.
spec.dormancy.messagesets your own sentence.
Added
- Processes say when they have read a run.
lm run getshowsEVALUATED;
lm run get ID --wait-evaluatedwaits for it. - Workflow due dates.
spec.duealertsdecision.overdueonce per document and deadline. - New hooks and options:
fill-down(carries a value onto the empty cells below it),
fix-mojibake,parser.lines: true(one line, one row: fixed-width files and logs),
split-row escape:(EDIFACT) andposition:,regex-extract required:,to-uuid generate-from:,
parse-number integer: true. - Kotlin hooks from the operator's folder:
script: hooks:<name>.kts, loaded from
LUMNIK_HOOKS_DIRand compiled at apply. Each run records the scripts it ran and their SHA-256. - rest-generic
auth.kind: oauth2(client credentials).lm source rest from-openapidrafts
auth from the spec. - Entities:
columns.<col>.prefer:picks the source that wins a column;
columns.<col>.conflicts: falsestops reporting sources that progress.lm entity historyshows
who asserted a decision and why. - Filters:
lm inbox --process/--type,lm observations list --workflow, and
lm observations transitionsfor the transition journal. lm ask --previousasks a follow-up question.- SOAP
{{page}}pages a SOAP source.
Fixed
- No event is missed when transactions end out of order, by the live tail, webhooks, email or
processes. An event waits while an older write transaction stays open; the hub logs a warning
after two minutes. - A local or sftp file replaced with the same name and size is read again.
- A csv full refresh no longer counts changed rows as removed.
- An unreachable jdbc source gives one plain sentence on the run.
- A view over a column its entity lost answers
400naming the column, not500. - An entity apply or delete refreshes every tenant's schema card.
- A SearchIndex on a hub without a Search provider reports
not_installed, notbuilding. deploy/selfhost/up.shkeeps the overlays the stack was started with.
v1.9.0
A hub table now holds what its source holds: one row per key for a database table, the file's rows
and no others for a single CSV file. Dates keep their type through an entity, align across sources,
and read the same whatever zone the hub runs in. lm ui becomes the integrator's workbench: every
kind can be browsed, created and acted on without leaving it. A caller bound to a métier is now
bound everywhere: connectors, runs, dead letters, workflows and the decision inbox.
Before you upgrade
- Take a backup. This release carries six migrations (V70–V75), and migrations are
forward-only. Seedocs/operations/upgrade.md. - Plan for a lock at first boot. V73 rebuilds the unique key of every existing
t_*table as
(tenant_id, _row_hash): one index per table, and every table stays locked until the migration
commits. Time it on a copy if your tables are large. - Add
--forceto scripts that delete.lm connector delete,lm entity deleteandlm search rmnow ask you to typedelete. Without a keyboard they stop:aborted: no confirmation read (EOF) — pass --force in a script, and nothing is deleted. - Stop using
lm migration applyandlm mapping. Both are removed. The hub migrates at every
start; the mapping template surface acted on nothing (V70 drops its table). - Check manifests the validator now refuses. Each of these used to pass
lm validateand fail
every run:- a
jdbc-generictable with noschema, on the table or indefaults; - an
sftpcsv-file with a strict host key (the default) and nohost_key_fingerprint(the hint
gives thessh-keyscancommand for your host and port); - a REST endpoint with
pagination.kind: custom.
- a
- Helm: OpenTelemetry is now off by default, as on self-host. If your hub exported traces, add a
hub.extraEnventryQUARKUS_OTEL_SDK_DISABLED=false.
After you upgrade
- Watch the first run of each
jdbc-generictable. It moves the table to one row per key:
the newest version of each key stays, the others are removed, andlm run getreports how many.
Two different rows under one key now fail the run namingpk_columns: declare the source's real
primary key. A key combined with anon-conflictother thanSKIPis refused. - Watch the first run of each single-file CSV connector. A
csv-filereading one local file
(no glob,after_process: none) now removes the rows the file no longer holds. A run that would
remove more than 30% of the table fails and removes nothing;spec.refresh.max_removedchanges
the limit (offdisables it). - Re-apply your entities (
lm entity apply -f …, same manifest). A view is compiled at apply,
so an existing entity keeps its old rules until then. After it:- each source key is read from the newest run that brought it, so an updated source row no
longer conflicts with its own old version; - a column every source holds as a date stays a
date, and dates mixed with timestamps become a
timestamptz(a date is its midnight UTC). A column with one text source stays text; - a source's day and another's time on that same day agree instead of conflicting;
- a join key that is a date or a time finds a day-keyed lookup row.
- each source key is read from the newest run that brought it, so an updated source row no
- If your hub does not run in UTC, expect dates to read differently. Views, view filters,
process rules and messages, entity reads and raw rows now all read and write in UTC, so the same
filter picks the same rows everywhere. The published Docker image runs in UTC. - If a webhook or n8n flow parses timestamps by exact format, check it. View JSON and the
process payloadfieldsnow carry2026-07-01T00:00:00.000500Z(microseconds kept) instead of
2026-07-01T00:00:00.000+00:00. Both are RFC 3339. - Review notifications and webhooks if you use
scope:*roles. Notification rules,
integrations, deliveries and the event stream are now tenant-wide administration: a caller
bound to a métier is refused them. Existing subscriptions keep firing;lm notify listand
lm integration listshow what is there. - A credential now leaves with the last connector that uses it. The delete confirmation names
it. - SOAP
{{secret:NAME}}now readslm secretfirst, then the environment. A tenant secret
outranks a hub variable of the same name; write{{env:NAME}}to read the environment only.
Added
lm ui, the integrator's workbench. New pages::entities(rows, identities, history),
:tables(thet_*tables with real row counts and their raw rows),:views,:apps,
:workflows,:exports,:search,:integrations,:notify,:secretsand:context(switch
hub without leaving).:newcreates every kind, validating before it applies. A JDBC source is
onboarded from the TUI, a REST endpoint is pre-flighted and probed there, and/filters most
lists. Every action runs off the screen's loop: a slow hub no longer freezes a key.- A jdbc connection declares its zone.
spec.connection.zone: Europe/Parisreads a source's
timestamp(no time zone) in that zone, each time at its own offset. Without it, such times are read as UTC, as before. To move rows already landed:
lm apply,lm endpoint reset-cursor,lm connector run. - jdbc
source.tables[].targetnames the hub table a source table lands in. - Delete says what goes before it asks.
lm connector deletenames each table, its rows and the
credential that goes with it;--dry-runon connector delete andlm endpoint purgeshow it
without deleting. - The audit trail records connector, entity, view, workflow and process writes.
- New commands:
lm entity list,lm app get NAME(an applyable manifest),lm view NAME [KEY](a saved view by name),lm outputs replay ID. - Settings that reach the hub: rate limits, the workflow sweep and OpenTelemetry can now be set
through.envon self-host andhub.extraEnvon Helm.
Changed
- A métier-bound caller is bound everywhere. Connector doors (get, update, delete, run, pause,
endpoints, tags) act only on connectors whose scopes the caller holds, and lists are filtered;
so are runs, dead letters, thelm sourcedoors, workflow graphs and tape, and the decision
inbox. Another métier's object is the uniform403 {"error":"scope not granted"}. A caller with
noscope:*role keeps tenant-wide administration. - A second connector cannot write another connector's table. Its run fails naming the holder
and the way out (jdbc:target; csv: rename the connector). A sharing that existed before
keeps working, and deleting one of the sharers removes only its own rows. - A row identical to another tenant's is stored (it was silently dropped while the run counted
it). - A SearchIndex re-apply that removes or changes what is served is withheld until a rebuild of
the new declaration cuts over, and an index made invalid by an entity re-apply serves nothing.
lm search statusshowswithheldandinvalid. - The flat connector door (
lm connector apply -f) creates only what it can run:jdbc-generic,
rest-genericand unknown types are refused naminglm apply -f. - Errors stop quoting what they should not. A webhook delivery, a REST probe or run, a database
failure and a workflow apply now say the kind of failure and its SQLSTATE, never a message that
can carry a URL credential or a stored value. Rows written before keep their text. - Output:
lm config viewredacts tokens (--rawshows them);lm connector runprints one
run id per endpoint;lm dlq listshows whether an entry was replayed or discarded;lm view
says when the 1 000-row cap dropped rows;lm secret setwithout--descriptionkeeps the
stored one; thelm inboxheader readsWHEN. - Refusals that were 500s: a taken connector name is a 409 naming its holder; a malformed CSV
row in a one-shot ingest is a 400 naming its line; a body over 10 MB says the limit; an
integration inHMACorBEARERmode without a secret is refused; a schedule onlm source jdbc addmust be a 5-field cron.
Fixed
- A connector delete no longer removes a table another connector shares, or another tenant's rows.
- A process rule on a
DATEcolumn fires on the day it names, whatever the hub's zone. - Several replicas no longer copy an audit row twice.
lm migration statusno longer fails on a schema with no applied migration.- A SOAP or GraphQL endpoint without
methodis stored asPOST, the method it is sent with.
v1.8.1
What the documentation already showed is now in the binary: export files shaped for a partner,
corrections that name a row the way you see it, and a time zone you can declare. And a date and
time with no time zone is now stored the same way wherever the hub runs.
Before you upgrade
- Nothing to do. No migration, no new setting.
After you upgrade
- If your hub runs from the jar on a machine not set to UTC, read this. A date and time with no
time zone used to be stored in that machine's zone:23:30landed as21:30UTC on a machine in
Paris in summer. It is now stored as UTC. This concernsparse-datewith a time, and a
jdbc-genericsource column of typetimestamp.- The published Docker image runs in UTC and was never affected.
- Rows already landed keep their value until their source row changes.
- If the source's times are in a known zone, say so:
parse-date: { …, zone: Europe/Paris }.
Added
- Exports shaped for a partner. A column can be renamed in the file (
as:) and formatted:
lookup(your codes to the partner's),format-date,format-number(decimal comma, fixed
decimals),mask(hide personal data).spec.filterkeeps only some rows. A value a formatter
cannot write fails the run and nothing is published. Existing export manifests are unchanged.
See Shape the file for a partner in the exports page. parse-datetakeszone:: say which time zone the source's times were written in.- Name a row by what you see.
lm entity assert-sameandlm entity assert-distincttake
<source table>:<source id>, the same pairlm entity historytakes:
lm entity assert-same Customer --left t_res_crm:9003 --right t_res_erp:C-003 --reason "…". - A guide to entity resolution for the installer, with every screen from a live hub
(Entities — resolve).
Changed
lm1.4.1 (the assertion handles above).
v1.8.0
The hub now sends data out as well as taking it in. Three capabilities land whole:
- Entity resolution gives the same customer or product one identity across systems that never
shared a key, and lets a person correct it. - Managed exports deliver an entity as CSV to SFTP or S3, on a schedule or on demand, without
ever overwriting a file. - Queued runs let a busy endpoint wait for its turn instead of refusing.
Views read past their old 1 000-row cut. And a long list of hot paths stopped holding a database
connection or reading a whole table when they didn't need to.
Before you upgrade
- Set
LUMNIK_ENTITY_RESOLUTION_HMAC_SECRET. The hub refuses to start without it: 16
characters or more, not the placeholder.deploy/selfhost/up.shadds it to your existing.envfor you.- With Helm, set
secrets.entityResolutionHmacSecret. - Keep it with your other secrets: changing it later invalidates every resolution fingerprint and
every open page link in a view.
- Take a backup. This release carries 21 migrations (V49–V69), and migrations are forward-only.
Seedocs/operations/upgrade.md. - Delete any duplicate SearchIndex you don't want. If two SearchIndex manifests target the same
entity, the upgrade keeps the earliest valid one and purges the other. - Decide on webhook history. Deliveries older than 90 days are purged at the first 05:00 run
after the upgrade, and can no longer be replayed. SetLUMNIK_WEBHOOKS_DELIVERY_RETENTION_DAYS=0
to keep everything. - The database image is now
pgvector/pgvector:0.8.6-pg16in the compose and Helm files. - Re-running
deploy/helm/up.shover a live cluster now refuses. It used to regenerate every
secret, including database passwords the database never learned.
LUMNIK_ALLOW_SECRET_REGENERATION=trueis the explicit way through.
After you upgrade
- Re-apply some entities with the same manifest (
lm entity apply -f …): every entity that
declaresresolution:orjoins[], and any entity a SearchIndex observes. Their views changed
shape. - Rename reserved columns.
_entity_id,_resolution,_source_idand_handleare now
reserved column names; a manifest that uses one is refused at its next apply. - Check values landed by rewriting hooks. Before this release, rewriting cell hooks ran twice on
everycsv-fileandjdbc-genericchunk:regex-replacecould landX-X-42, andsplitcame
out mangled. Check data such hooks wrote. - Fix scripts that relied on
lmexiting 0. An unknown word after a command group (lm connector lst) now exits 1 instead of printing help and exiting 0.lm export runnow reports
accepted.
Added
- Entity resolution. In a
kind:Entitymanifest,resolution:declares how rows from different
sources become one entity: an ordered cascade of exact rules over normalised fields.- What it never does: an ambiguous match never merges, and a resolved link is never moved
silently. - Reviewing:
lm entity ambiguous NAMElists what needs a human, andlm entity history NAME SOURCE IDexplains a decision, including which entities were set aside and why. - Correcting:
lm entity assert-same/assert-distinctrecord a correction that survives
every replay. Retracting asamesplits the identity back off. - Replaying:
lm entity resolve NAMEreplays the whole entity. - In the TUI:
:ambiguousshows the same list. - In the entity view: rows carry
_entity_idand_resolution, and unresolved rows carry a
_handle, solm conflicts listcan name every row.
- What it never does: an ambiguous match never merges, and a resolved link is never moved
kind:Export— deliver an entity as CSV to SFTP or S3.- The file: deterministic bytes (UTF-8, CRLF, a stable order).
- Publishing: always atomic and never over an existing file. SFTP uses a
.partfile and a
rename, with the host key always verified; S3 uses a conditional multipart upload with
checksums. - Running: on a cron schedule (latest slot only, no back-fill) or on demand, with a ledger of
every run. - Commands:
lm export apply|list|get|run|retry|runs|status, andlm entity export NAME -o FILEfor a one-shot download. - Docs:
docs/apps/exports.md.
- Queued runs (
LUMNIK_RUN_QUEUE_ENABLED=true, off by default).- What changes: a manual connector run, or an on-demand export, that meets a run already in
progress waits asQueuedand starts when the endpoint is free. - Cancelling:
lm run cancelends a queued run. - Before turning it on: upgrade every replica first;
docs/operations/upgrade.mdsays why.
- What changes: a manual connector run, or an on-demand export, that meets a run already in
- Views read past 1 000 rows.
POST /api/views/renderpages withpageSizeand an opaque
cursor.- The app's Fiches tab: it pages, with the pager above and below the results, and filters
through typed controls built from each column's kind. - Filters that can never match on a column's type are refused with a 400, instead of
returning an empty table. Scientific notation (1e-5) is accepted. spec.links: a cell can lead to a filtered view or to an external page.
- The app's Fiches tab: it pages, with the pager above and below the results, and filters
joins[]onkind:Entity. A many-to-one lookup table enriches an entity after fusion: no
match gives NULL, several matches give NULL plus a conflict entry. Keys compare by numeric value
(1000021joins1000021.0000).- JDBC dialect providers. PostgreSQL is now a provider behind
lumnik-jdbc-spi, and a connector
resolves its dialect strictly: an explicitdialect:wins, otherwise exactly one provider must
accept the URL.docs/connectors/jdbc/writing-a-provider.mdexplains how to write one. - Choosing what an app shows.
spec.tabs: on akind:App, it lists the tabs that app shows. A tab this hub does not offer
comes back as a warning, never a refusal.- Seeing the result:
lm app listshows your apps, andlm module listshows each module
with the reason it is active or not. - Docs: "Choosing what your hub offers".
- Proposal dossiers.
POST /api/proposal/checkreplays a prospect's dossier against your real
data, read-only.lm proposal draftturns it into akind:Workflowdraft, and never applies it. - SearchIndex declarations.
lm search apply|rm|query|status|rebuildand the
titleField/linkskeys. This edition carries no Search provider, and
GET /api/search/capabilitysays so. - Webhook delivery retention.
LUMNIK_WEBHOOKS_DELIVERY_RETENTION_DAYS(default 90) bounds the
delivery journal, which used to keep every payload and response forever.
Changed
- Webhooks and email are sent after their batch commits, with no database transaction held
across the remote call.- Webhooks: delivery is at-least-once, and
X-Lumnik-Event-Idis the idempotency key.
HMAC/Bearer retries that used to fail now deliver. - Email: a crash between commit and send loses that batch's mails instead of sending them
twice. - Failures: one failing event, or a NUL byte in a receiver's answer, no longer stalls the
consumer.
- Webhooks: delivery is at-least-once, and
- The outbox drains far faster: about 6 200 events per tick instead of 100.
- Local CSV says when something is wrong instead of ending quietly.
- A missing cursor file: if the cursor's file is gone while later files wait, the run fails
and names the fix,lm endpoint reset-cursor <connector>/<endpoint>. - A file removed mid-run: a listed file removed before the run reaches it fails the run.
- Already-read files: a fully read, unchanged file is not read again.
- New files: the folder is listed once per run, so a file dropped mid-run waits for the next
run.
- A missing cursor file: if the cursor's file is gone while later files wait, the run fails
- Remote CSV (SFTP/S3) keeps one connection per run instead of one per chunk, and relists every
100 files. Your SFTP server's idle timeout and session cap now apply to a whole run. - Scheduled runs and exports are bounded per instance: 4 and 2, set with
lumnik.connector.max-concurrent-scheduled-runsandlumnik.export.max-concurrent-scheduled-runs.
Runs beyond the bound wait instead of failing on pool timeouts. - Faster where it was slow, each change measured in its PR: the runs list, the data freshness
indicator, the inbox, the delivery pages, JDBC chunk fetches, row hashing and date parsing, and
repeated REST reads of unchanged rows. - Inbox acks: only an ack written by
POST /api/inbox/{id}/ackfolds a decision; a hand-emitted
ack event no longer does. lm source jdbc discover --skip-patterndefaults to empty; the dialect applies its own
defaults.- The app no longer shows a Chat tab in this edition, where it never worked.
Testing
- OTK silver: 182 of 182 scenarios with 0 findings on this tree. The managed-export delivery case
needs a real SFTP destination and stays dormant without one. lmbinaries: this release shipslm1.4.0 (archives andchecksums.txtalongside).