Skip to content

Scheduling

Send a notification later, at a time with its offset. A Workflow holds it through deploys until then.

Schedule a send

schedule() takes everything send() does, plus the time to send at:

TypeScript
const { scheduledId, op } = await env.CHASKI.schedule({
  type: 'invoice_due',
  recipient: { id: user.id, email: user.email },
  payload: { invoice: 'INV-1042' },
  scheduledAt: '2026-11-01T09:00:00-05:00',
  // Scheduling the same key again moves the pending send instead of adding one.
  idempotencyKey: 'invoice-due:INV-1042',
});

scheduledAt is an ISO 8601 time with an explicit offset, Z or ±hh:mm. A time without one would name a different instant in every timezone, so Chaski rejects it rather than guess. A time in the past is rejected too, with five minutes of grace for clock skew.

When the time comes, the send runs as if you had called send() then: the contact’s preferences and the template are read at that moment, with the recipient and payload you scheduled.

Move a schedule

With an idempotencyKey, scheduling again updates the pending send instead of adding a second one. Derive the key from what the notification is about, never from the time, so a new time moves it. op in the result says what happened:

opMeaning
createdA new schedule.
updatedThe pending schedule with this key moved to the new time and content.
completedThe schedule with this key already went out; nothing changed.
cancelledThe new time is already past, so the pending schedule was retired instead of firing late.

Cancel

Cancel by the schedule’s id, its idempotencyKey, or a groupKey you gave several schedules:

TypeScript
await env.CHASKI.schedules().cancel({ idempotencyKey: 'invoice-due:INV-1042' });

// Every pending reminder for one trial, scheduled with groupKey: `trial:${trial.id}`.
await env.CHASKI.schedules().cancel({ groupKey: `trial:${trial.id}` });

The result says how many pending schedules it cancelled.

List what’s pending

TypeScript
const pending = await env.CHASKI.schedules().list({ recipientId: user.id });

list() returns pending schedules, soonest first, for one recipient or for everyone. The Schedules page in the dashboard shows the same list, and cancels from there too.

Updated Oct 30, 2026

MIT License · chaski.sh