One task per row
A reflex can run its function once for each row of a query. Each row is its own task, and succeeds, fails or is retried alone.
pnpm add @niscorp/tide @niscorp/prism @niscorp/strata zodThe engine, with a selection
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.
select is how Tide gets rows: it is handed the query a reflex carries and returns the rows. Here it returns three members. The effect throws the first time it is asked to write to Linus.
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';
let tries = 0;
const tide = createTide({
store: createMemoryStore(),
transform: evaluate,
select: () => [{ id: 'ada' }, { id: 'linus' }, { id: 'mia' }],
effects: {
'mail.send': {
writes: [],
run: (input) => {
const { to } = input as { to: string };
if (to === 'linus' && (tries += 1) === 1) throw new Error('the mail server did not answer');
return { sent: to };
},
},
},
});
// 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`);A reflex with a selection
select names the query, and unitKey the field that tells one row from another. With mode: 'each' Tide makes one task for each row, and $.row is that row in the effect’s input.
await tide.load(
[
{
id: 'theatre.reminder',
intent: 'Remind every member with a seat tonight.',
on: { clock: { every: 'day', at: '12:00', tz: 'Europe/Vienna' } },
select: { query: 'members with a seat tonight', mode: 'each', unitKey: 'id' },
effect: { name: 'mail.send', input: { to: { $ref: '$.row.id' } } },
},
],
{ at: vienna(23, '09:00') },
);Noon, and a minute later
At noon two mails go out and one throws. An effect that throws is retried with backoff: after 1, 2 and 4 minutes. One that returns is done, whatever it returned.
Tide stores one run and three tasks. ledger reads them.
const printLedger = async (when: string): Promise<void> => {
const [run] = await tide.ledger.runs({});
const tasks = await tide.ledger.tasks({});
console.log(when, '· run', run?.state, '· done', run?.done, 'of', run?.total);
for (const task of tasks) console.log(' ', task.unit, task.state, 'attempt', task.attempt, task.error ?? '');
};
await tide.advance({ now: vienna(23, '12:00') });
await printLedger('12:00');
await tide.advance({ now: vienna(23, '12:01') });
await printLedger('12:01');Run it.
At noon Ada and Mia are done and Linus is waiting for a retry. A minute later his second attempt goes through, and the run is settled with three of three.
npx tsx rows.mts12:00 · run fanned · done 2 of 3 ada done attempt 1 linus retrying attempt 1 the mail server did not answer mia done attempt 1 12:01 · run settled · done 3 of 3 ada done attempt 1 linus done attempt 2 mia done attempt 1