Trigger on a database write
A reflex triggered by a row being inserted, and a second reflex triggered when the first one finishes: a workflow of two steps.
pnpm add @niscorp/tide @niscorp/prism @niscorp/strata zodThe engine
createTide makes the engine. It takes a store for its state (this one is in memory), Prism’s evaluate to fill in templates, and the effects: the functions a reflex may call by name.
Tide takes the time as an argument and never reads the system clock, so this file prints the same on any day.
import { createTide, createMemoryStore } from '@niscorp/tide';
import { evaluate } from '@niscorp/prism';
const tide = createTide({
store: createMemoryStore(),
transform: evaluate,
effects: {
'mail.send': { writes: [], run: (input) => console.log('mail.send', input) },
'board.update': { writes: [], run: (input) => console.log('board.update', input) },
},
});
// an instant: a day of October 2026 and a local time in Vienna
const vienna = (day: number, time: string): number => Date.parse(`2026-10-${day}T${time}:00+02:00`);The host’s loop
One advance processes what was passed in and runs what is due. A reflex triggered by one of those runs, such as the next one in a chain, runs on the next advance. So a host calls it until a pass does nothing.
// what a host does: advance until a pass does nothing
const settle = async (now: number): Promise<void> => {
for (;;) {
const pass = await tide.advance({ now });
const did = pass.factsMatched + pass.runsCreated + pass.tasksCreated + pass.executed + pass.runsSettled;
if (did === 0) return;
}
};Two reflexes
The first is triggered by a write: a row inserted into bookings. The row is $.row in its input.
The second is triggered when a run of the first has finished. Its on names the first reflex by id.
await tide.load(
[
{
id: 'booking.confirm',
intent: 'Confirm a booking to the member who made it.',
on: { fact: { entity: 'bookings', op: 'insert' } },
effect: { name: 'mail.send', input: { to: { $ref: '$.row.member' }, seats: { $ref: '$.row.seats' } } },
},
{
id: 'booking.board',
intent: 'Update the foyer board when a confirmation has gone out.',
on: { fact: { run: 'booking.confirm' } },
effect: { name: 'board.update', input: { confirmed: { $ref: '$.fact.stats.done' } } },
},
],
{ at: vienna(23, '09:00') },
);A booking is written
ingest passes Tide an event, here a write. It only stores it, and advance does the work.
In a Moss app with facts configured, nobody calls ingest for a write: each row written through Vex is passed to Tide.
await tide.ingest({ kind: 'write', entity: 'bookings', op: 'insert', row: { member: 'mia', seats: 2 }, at: vienna(23, '10:15') });
await settle(vienna(23, '10:15'));
for (const run of await tide.ledger.runs({})) console.log(run.reflexId, run.state, 'because of', run.cause);Run it.
The mail goes out, then the board is updated with one confirmation. Each run records what triggered it: the write, then the first run finishing.
npx tsx write.mtsmail.send { to: 'mia', seats: 2 }
board.update { confirmed: 1 }
booking.confirm settled because of fact:fact_3
booking.board settled because of fact:fact_6