Tide · guide

Save a timer

An AI can draft a reflex from a sentence: “in 40 minutes”. Saving it turns that into a date and time, and the bell still rings if the server restarts in between.

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

A draft

A draft is a reflex as a model writes one: it may say timer where a reflex says clock, and it cannot say who it runs as. @niscorp/tide/agent has the agent that writes drafts from words. This file writes one by hand, as that agent would.

A draft
import { anchorDraft, createTide, createMemoryStore } from '@niscorp/tide';
import type { ReflexDraft } from '@niscorp/tide';
import { evaluate } from '@niscorp/prism';

const draft: ReflexDraft = {
  id: 'interval-bell',
  intent: 'Ring the bell five minutes before the second half.',
  on: { timer: { minutes: 40 } },
  effect: { name: 'bell.ring', input: { text: 'Five minutes to the second half.' } },
};
Step 2 of 4

Saving it

anchorDraft is what a save does. It takes the time of the save, and turns “in 40 minutes” into a date and time.

Who the reflex runs as is added by your app at the save. A model cannot choose it.

Saving it
const savedAt = Date.parse('2026-10-24T19:00:00+02:00');
const reflex = { ...anchorDraft(draft, { at: savedAt, tz: 'Europe/Vienna' }), as: 'stage-manager' };
console.log(JSON.stringify(reflex.on), '· as', reflex.as);
Step 3 of 4

An engine, and a restart

Your app stores the reflex, and Tide’s state is in its store. So a restart is a new engine on the same store, with the reflex loaded again.

With Moss the store is Postgres. Here it is the one in memory, kept across the two engines.

An engine, and a restart
const store = createMemoryStore();
const engine = () =>
  createTide({
    store,
    transform: evaluate,
    effects: { 'bell.ring': { writes: [], run: (input, ctx) => console.log('bell.ring', input, '· as', ctx.actor) } },
  });

const first = engine();
await first.load([reflex], { at: savedAt });
await first.advance({ now: savedAt });

const restarted = engine();
await restarted.load([reflex]);
Step 4 of 4

The bell rings once

The second engine is called at 19:40 and the bell rings. Called again at 19:41, it does not ring again: that run is already recorded.

The bell rings once
await restarted.advance({ now: savedAt + 40 * 60_000 });
await restarted.advance({ now: savedAt + 41 * 60_000 });
console.log('runs:', (await restarted.ledger.runs({})).length);
Then

Run it.

The timer became a date and time, 19:40:00 in Vienna. The bell rang once, as the stage manager, in an engine that was started after the save.

Run it
npx tsx timer.mts
It prints
{"clock":{"at":"2026-10-24T19:40:00","tz":"Europe/Vienna"}} · as stage-manager
bell.ring { text: 'Five minutes to the second half.' } · as stage-manager
runs: 1