Skip to content

kind: Process — react to your data

The second rung: a declarative watcher on ingested rows. When a row lands that matches your conditions, the process emits a decision.* event — and the effect IS the event: delivery rides the hub's existing webhook subscriptions (n8n, Make, Zapier, your endpoint). Zero new code paths.

The real manifest (from the WOW demo — proven live)

# The dragon's first rung, PROVEN LIVE: when a new customer from Lyon lands in the CSV,
# emit a decision event. The effect IS the event — delivery rides the existing webhook
# subscriptions (n8n/Make/Zapier), zero new code.
#
# Why `ville` and scope `demo` (not `segment`/`clients`): a process triggers on
# data.row.changed, born at the RAW write path — so on.table is the raw ingest table
# `connector.t_cust_csv`, and its columns are the CSV's own (code, cname, email, ville).
# `segment` is a Siebel field, absent from the CSV. The raw table lives in the `demo` scope
# card (the csv connector is tagged `demo`); the fused `clients` card names only v_customer.
apiVersion: apps.lumnik.io/v1
kind: Process
metadata:
  name: lyon-client-arrive
spec:
  scope: demo
  on:
    event: data.row.changed
    table: connector.t_cust_csv
  when:
    - ville=Lyon
  emit:
    type: decision.lyon-client-arrive
    message: "Nouveau client {cname} à {ville}"

Fields

Field Required Meaning
metadata.name yes kebab-case ([a-z0-9-]+)
spec.scope yes the scope whose card declares on.table
spec.on.event yes must be data.row.changed — the only v1 trigger
spec.on.table yes the raw ingest table (connector.t_<name>) — see the note below
spec.when[] yes (≥1) conditions in the view filter grammar, ANDed
spec.emit.type yes must start with decision.
spec.emit.message yes human message, {col} templated

That table is the whole surface: apply refuses a key it does not honor and names it, so a misspelled mesage: costs you a 400 rather than a decision that fires with no text. The apiVersion: envelope is accepted and ignored.

lm process apply -f lyon-process.yaml
lm process list

In the TUI, :processes lists the declared rules; e re-opens the stored manifest through the :new authoring loop (edit → field-by-field validation → apply). The stored manifest is also retrievable raw: GET /api/processes/{name}/manifest.

The whole /api/processes surface (apply, list, manifest) is the admin door — the lm_admin role: declaring an attention rule is tenant administration, not a métier gesture. Unlike views, entities, and chat, it is not métier-scope-bound — there is no 403 {"error":"scope not granted"} here; the admin sees the whole tenant.

Every stored process carries an enabled flag (default true; the ENABLED column in lm process list), and only enabled processes fire. There is no API or CLI gesture to toggle it yet — disabling is a database-level edit.

Trigger at the raw write path

data.row.changed is born where rows are written — so on.table is the raw table the connector fills, not a fused view, and when columns are that table's own columns. The scope must be the one whose card carries that raw table (in the demo: the CSV connector's demo tag, not the fused clients scope).

Message templating

{col} interpolates the row's value ("Nouveau client {cname} à {ville}"). A null value renders as an empty string; there is no escape syntax — a placeholder is exactly {letters_digits_}, anything else ({}, {a-b}, a lone brace) passes through literal. Every {col} is validated at apply time against the trigger table: an unknown placeholder is refused (message references {x} — not a column of …), never discovered silently at fire time. Every referenced column also rides in the event payload as fields, so the receiving automation gets the data, not just the sentence.

Honesty & failure contract

  • Refusable at apply: structural errors, an unknown when column, or an out-of-domain value are refused with the card's hint — a process that could never fire is never accepted silently.
  • Fail-closed per process at runtime: a process that breaks (e.g. its table vanished) logs and skips — it never blocks the event cursor or its sibling processes.
  • Declarative only, by doctrine: there is no Kotlin escape hatch for processes. If a real métier process can't be expressed declaratively, that limitation should surface — not be hidden inside a script (the substrate breathes, it does not mutate).

See also

  • Inbox — where a human sees and disposes of the emitted decisions.
  • Workflows — declares what a status column's values mean, so a when: statut=TERMINE condition matches a state someone actually declared; a process fires on a row landing, but silence — nothing landing — is the workflow's dormancy sweep to see.
  • Kafka connector — events into the hub.