Triggers
Run something whenever a declared set of rows changes.
A trigger watches a set of rows and runs when that set changes — no schedule, no polling.
import { trigger } from "@highx/core";
export const onUrgentNote = trigger({
name: "notes.onUrgent",
watch: { table: "notes", index: "byColor", equals: { color: "red" } },
handler: async (ctx, event) => {
for (const note of event.entered) await notify(ctx, note);
},
});The event
The handler receives what changed, not the contents of the set:
| Field | |
|---|---|
entered | Rows that now match and did not before |
left | Rows that matched and no longer do, with the value they had |
updated | Rows that matched throughout and changed |
Why the watch is declared, not computed
The obvious way to build "run when something changes" is to re-run a query on every write. That cost grows with writes and with triggers, and it is the implementation this design exists to avoid.
Instead a watch is an ordinary indexed read, so it records the same dependency keys a query would. After a commit, the keys the write emitted are matched against the keys the watch recorded. A write that cannot have touched the watched set costs one key comparison; only a match re-reads and diffs.
That is also why the predicate is restricted: it must name a declared index and constrain its leading fields in order, optionally bounding the field that follows — exactly the shapes a dependency key can express.
A watch the compiler cannot read fails the build
// unsupported_trigger_predicate
export const onSomething = trigger({
name: "notes.onSomething",
watch: buildWatch(),
handler: () => undefined,
});A predicate assembled at runtime cannot be checked, and an unchecked watch is the unbounded scan this design avoids. An index the schema does not declare is refused when the trigger is registered.
Handlers are isolated
A trigger runs after its mutation has committed. A handler that throws cannot fail the mutation and does not stop the other triggers — failures are reported through the registry's error callback and the run continues.
A trigger handler is not part of the mutation's transaction. Work that must commit atomically with the write belongs in the mutation itself.