Skip to main content
Version: 0.1.33

Table Triggers

A table trigger reacts to a row event on a source table by running a Lua fire(row, prev, event) that returns a list of operations — and the engine applies each one to another table: append a point, update a state row, delete a key. One trigger can write to many tables of any kind at once — alarms, audit trails, derived events, materialized rollups. It is the sibling of Calculated Fields: both drain a table and run Lua per row, but a calculated field enriches the row in place, while a trigger writes to other tables.

TableTrigger(vehicle_events) → TriggerRules(vehicle_events)

The node drains the source and does its own writes; there is no TableSink. Definitions live as data in x_triggers, fetched fresh every run — a save is a hot reload, no restart, no republish.

A definition is name + triggers + events + script

partmeaning
namea name for the rule
triggersrun only when one of these columns arrives (raw) or changes (a computed sibling); empty = every row
on eventsrun only on insert / update / delete; empty = all
scriptthe Lua below

There is no single target: each op the script returns carries its own table, so one trigger fans out wherever it needs to.

Writing the script

Define fire(row, prev, event) and return an op, a list of ops, or nil for nothing. Each op is a table with a verb and the table it acts on. You may declare helper functions and constants alongside fire (a bare body is wrapped for you):

function fire(row, prev, event)
if row.rpm ~= nil and row.rpm > 5000 then
return {
{ op = 'insert', table = 'alarm_events',
row = { entity = row.entity, ts = row.ts, level = 'high', msg = 'overrev' } },
{ op = 'update', table = 'vehicle_state',
set = { overrev = 1 }, where = { xid = row.entity } },
}
end
end

The three verbs

verbneedsbecomestypical table kind
insertrow = {…}INSERT INTO t (cols…) VALUES (…)timeseries / rows
updateset = {…}, where = {…}UPDATE t SET … WHERE …rows
deletewhere = {…}DELETE FROM t WHERE …rows / kv

The engine resolves each verb by the target table's kind — a kv table matches on where = { key = … } and a rows table on where = { xid = … }, an insert into a timeseries appends a point. The node itself stays kind-agnostic: it only builds valid SQL. where clauses are AND-joined; nil cells in a row are dropped (sparse, schema-on-write). Returning a single op instead of a one-element list is accepted as a convenience. No op may target the trigger's own source table (that would feed its own drain).

prev, event and state

prev, event and state behave exactly as in a calculated field:

  • prev is this entity's previous row (its trigger columns), so a crossing fires once instead of on every row above the line:

    function fire(row, prev, event)
    if (prev.rpm or 0) <= 5000 and row.rpm > 5000 then
    return { op = 'insert', table = 'alarms',
    row = { entity = row.entity, ts = row.ts, msg = 'crossed 5000' } }
    end
    end
  • event is "insert" (first sighting) / "update" / "delete". prev and event advance only forward in time, so a replay or the pipeline's own feed echo never fires a trigger twice.

  • state is a per-entity table that survives across runs (persisted in x_trigger_state).

The console

Under each table the sidebar has a Triggers section: New trigger on its menu, and each trigger an Edit / Delete menu (delete stops the trigger; rows it already wrote stay). The editor is a workspace tab: name, trigger columns, on-events, and the Lua editor.

Everything is plain SQL on x_triggers, so automation can do the same:

INSERT INTO x_triggers
(name, source_table, script, version, enabled, trigger_cols, on_events)
VALUES ('overrev', 'vehicle_events',
'return row.rpm and row.rpm > 5000 and { op=''insert'', table=''alarms'', row={entity=row.entity, ts=row.ts, msg=''overrev''} } or nil',
1, true, '["rpm"]', NULL);

A trigger needs a published pipeline whose TableTrigger names the source table and whose TriggerRules targets it:

{
"version": 1, "name": "vehicle-triggers",
"nodes": [
{"id": "trig", "type": "TableTrigger", "config": {"table": "vehicle_events"}},
{"id": "rules", "type": "TriggerRules", "config": {"table": "vehicle_events"}}
],
"edges": [ {"from": "trig.rows", "to": "rules.in"} ]
}