Skip to content

Releases

v1.11.2

Choose a tag to compare

@icreated icreated released this 09 Nov 19:56

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

Choose a tag to compare

@icreated icreated released this 09 Nov 17:23

v1.11.1 — 2026-11-09

The README opens on a real terminal: six lm commands, replayed on a Siebel over SOAP and a CSV
export, from the first run to a question answered with its SQL. No code change, no migration, lm
stays at 1.8.0.

v1.11.0

Choose a tag to compare

@icreated icreated released this 09 Nov 10:55

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 plain CREATE INDEX on a new table:
    the build is short). No action. The next free number is V91.
  • A rest-generic run that ended Completed with zero rows can now end Failed. A
    pagination.response_path that reads nothing on the first page fails with pagination.response_path reads nothing on the first page — check it against the source's answer; one that reads an object
    fails with pagination.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 longer NUMERIC(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 read 1024944, not 1024944.0000. Check any view or query that counted on four
    decimals.
  • parse-date with a yy format needs century-from. 98 was read as 2098 without a word.
    Set century-from to the first year of your 100-year window (1950 covers 1950 to 2049).
    lm validate refuses the hook without it, and refuses century-from when no format reads yy.
  • Event patterns are strict (mail rules, webhooks). A * is accepted only as a final .*, and
    prefix.* needs the dot: decision.cold.* no longer matches decision.coldroom-x. Create and
    update answer 400 with event 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 on notification.* never receives the new notification.mail.given-up
    event (loop guard): carry it with a webhook.
  • A decision mail is subjected by the decision's sentence (the message lm inbox shows, cut at
    100 characters). Other mails keep lumnik event: <type>.
  • A Process when: that compares to a {x} literal now means the column x. Apply refuses it if
    x is 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: ingestion on a REST table. Apply refuses it with a
    sentence saying why. A Workflow can still watch a REST table.
  • A catch-all data.row.changed subscription (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 apply also warns when a parse-date,
    parse-number or parse-boolean types a column the table holds as another type: the table is not
    retyped; delete the connector, apply, run.
  • A csv-file with no parser.encoding and bytes above 0x7F now ends the run Partial. The run's
    error: says the encoding was guessed, and asks you to declare parser.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, then lm endpoint reset-cursor.
  • A csv-file header is trimmed. A header padded with spaces (a Firebird isql export) 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 ended Failed. Add clean: { 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.csv lands in
    ext.clients, not ext.clients_csv. A table already created as ext.<name>_csv keeps its name,
    and a new ingest of the same file creates ext.<name>. Check queries and Entities that read the old
    name.
  • A cron slot outside the schedule window is skipped, not run late at the first in-window tick:
    0 6 * * * with window 22:00-06:00 used 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-01 can 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 resolve leaves the rows an
    exact rule resolved ambiguous, with no entity id; a pair declared with lm entity assert-same
    keeps its link. If it changed by accident, put the old secret back before the next lm 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 required no longer says (kebab-case): nothing enforced that case. To take
    over a one-shot csv ingest, use the name lm connector list shows.

Added

  • A REST connector's rows can feed a Process and a Workflow. Write a kind: Process or
    kind: Workflow on an ext.* 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 inbox then 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: set spec.on.table to 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 apply and lm entity apply print each hub warning on stderr as warning: …. 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 (optional consumer=, open=true; the 200 most recent)
    lists them; POST /api/platform/events/dead-letters/{id}/replay redoes one (Process, webhook and mail
    letters). A failed replay answers 500 and leaves the letter open. No lm command 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 ID sends a
    given-up mail again. A sent mail answers 409, another tenant's 404. HTTP:
    GET /api/platform/notification-deliveries and POST …/{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_date reads /Date(<ms>)/ and /Date(<ms>±<minutes>)/ (OData v2) as a
    UTC timestamp. Pagination presets odata-v2 and odata-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 as items 0-19/57 (a * total is refused).
    `http_version: "1.1...
Read more

v1.10.3

Choose a tag to compare

@icreated icreated released this 04 Nov 00:36

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 Completed can now end Failed. Each case lost rows in
    silence. Run lm validate on 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 presets nestjs, spring-page,
      spring-paged-model, micronaut, fastapi, spring-hateoas and drf-page.
  • Manifests refused at lm validate that passed before.
    • A path: query that names the pagination's own parameter (path: /items?size=500 with
      kind: fastapi). Before, the parameter was sent twice.
    • A promote entry 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: " " with mode: watermark).
    • An invalid default_rate_limit is no longer ignored after the first use.
  • Wording moved. A script that reads these must follow: the lm run get cell-error line now reads
    N cell(s) not delivered as declared (…) — the rows were still written; a bearer auth missing its
    token is reported at spec.auth; index errors carry spec.target.indexes[n].columns.
  • Stored values. A CSV header named row_hash stays in data and 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 _csrf and Tomcat
      j_security_check shapes), holds the cookie per connector and tenant, and logs in once more and
      replays when it expires.
    • response_format: xml reads an XML answer. Attributes sit under @, text under #text; a DTD
      and external entities are refused.
    • The preset prestashop.
    • On the page strategy, total_path with size_param ends 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 own true: / false: lists) gives a real boolean column.
  • lm conflicts list --entity NAME (and GET /api/entities/conflicts?entity=NAME). An unknown
    entity is 404; an entity outside your scope is the uniform 403.
  • lm describe connector-type prints the rules of each key (required when, ignored unless,
    one of, at least one of, element shapes) and default:. -o json keeps default.

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 probe drafts id_2 instead of a reserved promote name.
  • regex-replace and the constant patterns of CSV inference compile once.
  • Process and Workflow manifests accept a spec.scope with spaces around it, stored stripped, as the
    other kinds already did.

v1.10.2

Choose a tag to compare

@icreated icreated released this 01 Nov 23:44

See CHANGELOG.md in this tree for what changed in v1.10.2.

v1.10.1

Choose a tag to compare

@icreated icreated released this 25 Oct 20:21

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.sh no longer writes a fresh .env over an earlier install's volumes.
    The volumes keep the passwords of the .env that 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, or docker compose -p <project> … down -v, which deletes
    the data. If Docker cannot list the volumes, it stops too.
  • lm ask, lm chat, lm rag search and lm scopes say 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, lm and TUI help, docs. Two kind:Workflow apply refusals now read
    belongs to another scope and in 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 with columns:, or parser.lines: true).

v1.10.0

Choose a tag to compare

@icreated icreated released this 25 Oct 18:18

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.
    See docs/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.
  • Run lm validate on 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 under spec.mapping, a column that is not declared
      (a headerless file with columns:, or parser.lines: true; a header read at run time is not checked),
      a value outside a knob's allowed values, two regex-extract writing the same column;
    • a boolean written yes, no, on or off: write true or false;
    • a mapping: block on jdbc-generic or rest-generic: those rows land as the source holds
      them, remove the block;
    • in rest-generic spec.defaults: base_url, auth, default_rate_limit, graphql_path or a
      key that switches the request shape;
    • a GraphQL incremental.param the query does not declare;
    • a Kotlin script: that is not classpath: or hooks:, and column: on a kotlin hook;
    • an unknown SOAP {{placeholder}}, an unknown on-error mode.
  • Fix scripts that parse lm output.
    • lm apply -f, lm process apply and lm workflow apply print one line, not JSON.
    • lm -o yaml uses the JSON key names (recordsIn, cellErrors).
    • lm inbox prints [acked] and English ages.
  • Fix scripts that close dead letters. lm dlq replayed needs --new-run-id: the run that
    carried the row. An entry no run carried is closed with lm dlq discard. Past REPLAYED entries
    with no run become DISCARDED.
  • 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, not entity_filter).
    • GET /api/entities/{name} answers 400 for a filter on a column it cannot filter. Before, the
      filter was ignored and every row came back.
    • A 409 from the identity and secret doors now carries {"error": …}. Before, it was empty.
  • Helm: authentication is on by default. auth.mode is oidc. dev-bypass renders only with
    auth.iUnderstandNoAuth=true.
  • A REST header value_env that resolves to nothing stops the run. It reads lm secret first,
    then the environment. The run stops with Header X unavailable (env=Y). Before, the header was
    left out.

After you upgrade

  • Columns declared on-error: dlq or fail now 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 context values keep their YAML types. A script that casts a number as String fails.
  • 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: 0s restores 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
    as numeric. 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.message sets your own sentence.

Added

  • Processes say when they have read a run. lm run get shows EVALUATED;
    lm run get ID --wait-evaluated waits for it.
  • Workflow due dates. spec.due alerts decision.overdue once 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) and position:, 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_DIR and 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-openapi drafts
    auth from the spec.
  • Entities: columns.<col>.prefer: picks the source that wins a column;
    columns.<col>.conflicts: false stops reporting sources that progress. lm entity history shows
    who asserted a decision and why.
  • Filters: lm inbox --process/--type, lm observations list --workflow, and
    lm observations transitions for the transition journal.
  • lm ask --previous asks 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 400 naming the column, not 500.
  • An entity apply or delete refreshes every tenant's schema card.
  • A SearchIndex on a hub without a Search provider reports not_installed, not building.
  • deploy/selfhost/up.sh keeps the overlays the stack was started with.

v1.9.0

Choose a tag to compare

@icreated icreated released this 20 Oct 15:00

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. See docs/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 --force to scripts that delete. lm connector delete, lm entity delete and lm search rm now ask you to type delete. Without a keyboard they stop: aborted: no confirmation read (EOF) — pass --force in a script, and nothing is deleted.
  • Stop using lm migration apply and lm 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 validate and fail
    every run:
    • a jdbc-generic table with no schema, on the table or in defaults;
    • an sftp csv-file with a strict host key (the default) and no host_key_fingerprint (the hint
      gives the ssh-keyscan command for your host and port);
    • a REST endpoint with pagination.kind: custom.
  • Helm: OpenTelemetry is now off by default, as on self-host. If your hub exported traces, add a
    hub.extraEnv entry QUARKUS_OTEL_SDK_DISABLED=false.

After you upgrade

  • Watch the first run of each jdbc-generic table. It moves the table to one row per key:
    the newest version of each key stays, the others are removed, and lm run get reports how many.
    Two different rows under one key now fail the run naming pk_columns: declare the source's real
    primary key. A key combined with an on-conflict other than SKIP is refused.
  • Watch the first run of each single-file CSV connector. A csv-file reading 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_removed changes
    the limit (off disables 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.
  • 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 payload fields now carry 2026-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 list and
    lm integration list show what is there.
  • A credential now leaves with the last connector that uses it. The delete confirmation names
    it.
  • SOAP {{secret:NAME}} now reads lm secret first, 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 (the t_* tables with real row counts and their raw rows), :views, :apps,
    :workflows, :exports, :search, :integrations, :notify, :secrets and :context (switch
    hub without leaving). :new creates 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/Paris reads 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[].target names the hub table a source table lands in.
  • Delete says what goes before it asks. lm connector delete names each table, its rows and the
    credential that goes with it; --dry-run on connector delete and lm endpoint purge show 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 .env on self-host and hub.extraEnv on 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, the lm source doors, workflow graphs and tape, and the decision
    inbox. Another métier's object is the uniform 403 {"error":"scope not granted"}. A caller with
    no scope:* 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 status shows withheld and invalid.
  • The flat connector door (lm connector apply -f) creates only what it can run: jdbc-generic,
    rest-generic and unknown types are refused naming lm 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 view redacts tokens (--raw shows them); lm connector run prints one
    run id per endpoint; lm dlq list shows whether an entry was replayed or discarded; lm view
    says when the 1 000-row cap dropped rows; lm secret set without --description keeps the
    stored one; the lm inbox header reads WHEN.
  • 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 in HMAC or BEARER mode without a secret is refused; a schedule on lm source jdbc add must 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 DATE column fires on the day it names, whatever the hub's zone.
  • Several replicas no longer copy an audit row twice.
  • lm migration status no longer fails on a schema with no applied migration.
  • A SOAP or GraphQL endpoint without method is stored as POST, the method it is sent with.

v1.8.1

Choose a tag to compare

@icreated icreated released this 15 Oct 18:22

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:30 landed as 21:30 UTC on a machine in
    Paris in summer. It is now stored as UTC. This concerns parse-date with a time, and a
    jdbc-generic source column of type timestamp.
    • 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.filter keeps 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-date takes zone:: say which time zone the source's times were written in.
  • Name a row by what you see. lm entity assert-same and lm entity assert-distinct take
    <source table>:<source id>, the same pair lm entity history takes:
    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

  • lm 1.4.1 (the assertion handles above).

v1.8.0

Choose a tag to compare

@icreated icreated released this 15 Oct 12:07

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.sh adds it to your existing .env for 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.
    See docs/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. Set LUMNIK_WEBHOOKS_DELIVERY_RETENTION_DAYS=0
    to keep everything.
  • The database image is now pgvector/pgvector:0.8.6-pg16 in the compose and Helm files.
  • Re-running deploy/helm/up.sh over a live cluster now refuses. It used to regenerate every
    secret, including database passwords the database never learned.
    LUMNIK_ALLOW_SECRET_REGENERATION=true is the explicit way through.

After you upgrade

  • Re-apply some entities with the same manifest (lm entity apply -f …): every entity that
    declares resolution: or joins[], and any entity a SearchIndex observes. Their views changed
    shape.
  • Rename reserved columns. _entity_id, _resolution, _source_id and _handle are 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
    every csv-file and jdbc-generic chunk: regex-replace could land X-X-42, and split came
    out mangled. Check data such hooks wrote.
  • Fix scripts that relied on lm exiting 0. An unknown word after a command group (lm connector lst) now exits 1 instead of printing help and exiting 0. lm export run now reports
    accepted.

Added

  • Entity resolution. In a kind:Entity manifest, 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 NAME lists what needs a human, and lm entity history NAME SOURCE ID explains a decision, including which entities were set aside and why.
    • Correcting: lm entity assert-same / assert-distinct record a correction that survives
      every replay. Retracting a same splits the identity back off.
    • Replaying: lm entity resolve NAME replays the whole entity.
    • In the TUI: :ambiguous shows the same list.
    • In the entity view: rows carry _entity_id and _resolution, and unresolved rows carry a
      _handle, so lm conflicts list can name every row.
  • 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 .part file 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, and lm entity export NAME -o FILE for 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 as Queued and starts when the endpoint is free.
    • Cancelling: lm run cancel ends a queued run.
    • Before turning it on: upgrade every replica first; docs/operations/upgrade.md says why.
  • Views read past 1 000 rows. POST /api/views/render pages with pageSize and 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.
  • joins[] on kind: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
    (1000021 joins 1000021.0000).
  • JDBC dialect providers. PostgreSQL is now a provider behind lumnik-jdbc-spi, and a connector
    resolves its dialect strictly: an explicit dialect: wins, otherwise exactly one provider must
    accept the URL. docs/connectors/jdbc/writing-a-provider.md explains how to write one.
  • Choosing what an app shows.
    • spec.tabs: on a kind: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 list shows your apps, and lm module list shows each module
      with the reason it is active or not.
    • Docs: "Choosing what your hub offers".
  • Proposal dossiers. POST /api/proposal/check replays a prospect's dossier against your real
    data, read-only. lm proposal draft turns it into a kind:Workflow draft, and never applies it.
  • SearchIndex declarations. lm search apply|rm|query|status|rebuild and the
    titleField/links keys. This edition carries no Search provider, and
    GET /api/search/capability says 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-Id is 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.
  • 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.
  • 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-runs and lumnik.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}/ack folds a decision; a hand-emitted
    ack event no longer does.
  • lm source jdbc discover --skip-pattern defaults 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.
  • lm binaries: this release ships lm 1.4.0 (archives and checksums.txt alongside).