Tide · guide

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.

Install it
pnpm add @niscorp/tide @niscorp/prism @niscorp/strata zod
Step 1 of 3

The 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.

The engine
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`);
Step 2 of 3

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.

The reflex
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);
Step 3 of 3

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.

Advance to the next run
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'))));
Then

Run it.

The mail is sent once, at 18:30 Vienna time, which is 16:30 in the ISO lines.

Run it
npx tsx clock.mts
It prints
loaded: 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