A scheduled reflex
The reflex of the first steps, on a daily schedule, and the loop a host runs around it: ask Tide when the next run is due, and call it then.
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) } },
});
// 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 reflex
A reflex is data: an id, a sentence saying what it does, its trigger, and the one effect it calls.
This trigger is a schedule: every day at 18:30 in a named time zone. The effect’s input is a template, and $.occurrence.key is the day it is running for.
const loaded = await tide.load(
[
{
id: 'theatre.doors',
intent: 'Tell the front of house at 18:30 that the doors open in half an hour.',
on: { clock: { every: 'day', at: '18:30', tz: 'Europe/Vienna' } },
effect: { name: 'mail.send', input: { to: 'front-of-house', night: { $ref: '$.occurrence.key' } } },
},
],
{ at: vienna(23, '12:00') },
);
console.log('loaded:', loaded.loaded);Advance to the next run
nextDue returns when the next run is due. A host sleeps until then and calls advance with the current time.
Nothing happens at 17:00. At 18:30 the effect is called. A second advance at the same time finds nothing left to do, and the next time Tide wants waking is the next evening.
const iso = (instant: number | undefined): string => (instant === undefined ? 'never' : new Date(instant).toISOString());
await tide.advance({ now: vienna(23, '12:00') });
console.log('next due:', iso(await tide.nextDue(vienna(23, '12:00'))));
await tide.advance({ now: vienna(23, '17:00') });
console.log('17:00 passed');
await tide.advance({ now: vienna(23, '18:30') });
await tide.advance({ now: vienna(23, '18:30') });
console.log('next due:', iso(await tide.nextDue(vienna(23, '18:30'))));Run it.
The mail is sent once, at 18:30 Vienna time, which is 16:30 in the ISO lines.
npx tsx clock.mtsloaded: 1
next due: 2026-10-23T16:30:00.000Z
17:00 passed
mail.send { to: 'front-of-house', night: '2026-10-23' }
next due: 2026-10-24T16:30:00.000Z