Skip to main content
Workflows

Cron Jobs and Scheduled Tasks

Run a daily report on a cron schedule inside a Rivet Actor, then add one-shot and fixed-interval jobs that survive sleep, restarts, and deploys.

See the full example on GitHub. View on GitHub
Your appany clientreminderActoronCreate, sendDailyReportCron daily-report0 9 * * *c.statelastReportfirst call1cron.set2sendDailyReport daily at 9:003saves the report4dailyReport event5One per keyRivet ActorCallReply

Rivet Actor schedules are durable timers that live with the Actor. They survive sleep, restarts, upgrades, deploys, and crashes, so you do not need a separate cron service or a job queue to run work on time.

This guide adds a daily report to a reminders app. A reminderActor stores reminders that fire once, and a cron job on the same Actor sums them up every morning.

What we’ll build:

  • A reminderActor per key that stores reminders and the last daily report in its state.
  • A daily-report cron job, installed when the Actor is created, that runs every day at 9:00 Los Angeles time.
  • A sendDailyReport action that the job calls to build the report, save it, and broadcast it to connected clients.
  • One-shot reminders scheduled with c.schedule.after and c.schedule.at, and cancelled with c.schedule.cancel.

Install

npm add rivetkit @rivetkit/react

Create the reminder Actor

The Actor’s state holds the reminders and the bookkeeping the daily report needs:

import { actor, event, type ScheduledFireInfo, setup } from "rivetkit";

interface Reminder {
	id: string;
	scheduleId: string;
	message: string;
	scheduledAt: number;
	completedAt?: number;
}

interface DailyReport {
	from: number;
	to: number;
	completed: number;
	pending: number;
}

interface ReminderActorState {
	reminders: Reminder[];
	completedCount: number;
	lastReportAt: number;
	lastReport?: DailyReport;
}

const reminderActor = actor({
	state: {
		reminders: [] as Reminder[],
		completedCount: 0,
		lastReportAt: 0,
	} satisfies ReminderActorState as ReminderActorState,
	events: {
		reminderTriggered: event<Reminder>(),
		dailyReport: event<DailyReport>(),
	},

	// Install the daily report job when the Actor is created.
	onCreate: async (c) => {
		await c.cron.set({
			name: "daily-report",
			expression: "0 9 * * *",
			timezone: "America/Los_Angeles",
			action: "sendDailyReport",
		});
	},

	actions: {
		// Called by the daily-report cron job every day at 9:00.
		sendDailyReport: (c, fire: ScheduledFireInfo) => {
			const from = c.state.lastReportAt;
			const to = fire.scheduledAt;

			// Skip a run whose window was already reported.
			if (to <= from) return c.state.lastReport;

			const report: DailyReport = {
				from,
				to,
				completed: c.state.reminders.filter(
					(r) =>
						r.completedAt !== undefined &&
						r.completedAt >= from &&
						r.completedAt < to,
				).length,
				pending: c.state.reminders.filter(
					(r) => r.completedAt === undefined,
				).length,
			};

			c.state.lastReportAt = to;
			c.state.lastReport = report;
			c.broadcast("dailyReport", report);
			return report;
		},

		// Return the last report, the next run time, and recent runs.
		getDailyReport: async (c) => {
			const job = await c.cron.get("daily-report");
			const history = await c.cron.history("daily-report", { limit: 7 });
			return {
				lastReport: c.state.lastReport,
				nextRunAt: job?.nextRunAt,
				history,
			};
		},

		// Schedule a reminder with a delay in milliseconds
		scheduleReminder: async (c, message: string, delayMs: number) => {
			const id = `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
			const scheduleId = await c.schedule.after(
				delayMs,
				"triggerReminder",
				id,
			);
			const reminder: Reminder = {
				id,
				scheduleId,
				message,
				scheduledAt: Date.now() + delayMs,
			};

			c.state.reminders.push(reminder);

			return reminder;
		},

		// Schedule a reminder at a specific timestamp
		scheduleReminderAt: async (c, message: string, timestamp: number) => {
			const id = `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
			const scheduleId = await c.schedule.at(
				timestamp,
				"triggerReminder",
				id,
			);
			const reminder: Reminder = {
				id,
				scheduleId,
				message,
				scheduledAt: timestamp,
			};

			c.state.reminders.push(reminder);

			return reminder;
		},

		// Trigger a scheduled reminder
		triggerReminder: (c, reminderId: string) => {
			const reminder = c.state.reminders.find((r) => r.id === reminderId);
			if (!reminder) {
				console.warn(`reminder not found: ${reminderId}`);
				return;
			}

			// Mark as completed
			reminder.completedAt = Date.now();
			c.state.completedCount++;

			// Broadcast event
			c.broadcast("reminderTriggered", reminder);

			console.log(`reminder triggered: ${reminder.message}`);
		},

		// Get all reminders
		getReminders: (c): Reminder[] => {
			return c.state.reminders;
		},

		// Cancel a pending scheduled reminder.
		cancelReminder: async (c, reminderId: string) => {
			const reminder = c.state.reminders.find((r) => r.id === reminderId);
			if (!reminder) return { success: false };

			const cancelled = await c.schedule.cancel(reminder.scheduleId);
			if (cancelled) {
				c.state.reminders = c.state.reminders.filter(
					(r) => r.id !== reminderId,
				);
			}

			return { success: cancelled };
		},

		// Get statistics about reminders
		getStats: (c) => {
			const total = c.state.reminders.length;
			const completed = c.state.completedCount;
			const pending = total - completed;

			return {
				total,
				completed,
				pending,
			};
		},
	},
});

export const registry = setup({
	use: { reminderActor },
});

export type Registry = typeof registry;
export type { DailyReport, Reminder };

registry.start();
  • reminders and completedCount belong to the one-shot reminders you add in the last step. lastReportAt and lastReport belong to the daily report. All of it is state, saved automatically.
  • lastReportAt starts at 0, so the first report covers everything that happened before it.
  • events declares reminderTriggered and dailyReport, the two typed events the Actor broadcasts.
  • Every schedule belongs to one Actor, so the key decides the scope of the job. The example uses a single key, ["main"], which gives one global job. Use one key per user or per resource for reminders, trials, billing periods, and other per-entity jobs.

Install the daily job

onCreate installs the cron job the first time the Actor is created:

import { actor, event, type ScheduledFireInfo, setup } from "rivetkit";

interface Reminder {
	id: string;
	scheduleId: string;
	message: string;
	scheduledAt: number;
	completedAt?: number;
}

interface DailyReport {
	from: number;
	to: number;
	completed: number;
	pending: number;
}

interface ReminderActorState {
	reminders: Reminder[];
	completedCount: number;
	lastReportAt: number;
	lastReport?: DailyReport;
}

const reminderActor = actor({
	state: {
		reminders: [] as Reminder[],
		completedCount: 0,
		lastReportAt: 0,
	} satisfies ReminderActorState as ReminderActorState,
	events: {
		reminderTriggered: event<Reminder>(),
		dailyReport: event<DailyReport>(),
	},

	// Install the daily report job when the Actor is created.
	onCreate: async (c) => {
		await c.cron.set({
			name: "daily-report",
			expression: "0 9 * * *",
			timezone: "America/Los_Angeles",
			action: "sendDailyReport",
		});
	},

	actions: {
		// Called by the daily-report cron job every day at 9:00.
		sendDailyReport: (c, fire: ScheduledFireInfo) => {
			const from = c.state.lastReportAt;
			const to = fire.scheduledAt;

			// Skip a run whose window was already reported.
			if (to <= from) return c.state.lastReport;

			const report: DailyReport = {
				from,
				to,
				completed: c.state.reminders.filter(
					(r) =>
						r.completedAt !== undefined &&
						r.completedAt >= from &&
						r.completedAt < to,
				).length,
				pending: c.state.reminders.filter(
					(r) => r.completedAt === undefined,
				).length,
			};

			c.state.lastReportAt = to;
			c.state.lastReport = report;
			c.broadcast("dailyReport", report);
			return report;
		},

		// Return the last report, the next run time, and recent runs.
		getDailyReport: async (c) => {
			const job = await c.cron.get("daily-report");
			const history = await c.cron.history("daily-report", { limit: 7 });
			return {
				lastReport: c.state.lastReport,
				nextRunAt: job?.nextRunAt,
				history,
			};
		},

		// Schedule a reminder with a delay in milliseconds
		scheduleReminder: async (c, message: string, delayMs: number) => {
			const id = `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
			const scheduleId = await c.schedule.after(
				delayMs,
				"triggerReminder",
				id,
			);
			const reminder: Reminder = {
				id,
				scheduleId,
				message,
				scheduledAt: Date.now() + delayMs,
			};

			c.state.reminders.push(reminder);

			return reminder;
		},

		// Schedule a reminder at a specific timestamp
		scheduleReminderAt: async (c, message: string, timestamp: number) => {
			const id = `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
			const scheduleId = await c.schedule.at(
				timestamp,
				"triggerReminder",
				id,
			);
			const reminder: Reminder = {
				id,
				scheduleId,
				message,
				scheduledAt: timestamp,
			};

			c.state.reminders.push(reminder);

			return reminder;
		},

		// Trigger a scheduled reminder
		triggerReminder: (c, reminderId: string) => {
			const reminder = c.state.reminders.find((r) => r.id === reminderId);
			if (!reminder) {
				console.warn(`reminder not found: ${reminderId}`);
				return;
			}

			// Mark as completed
			reminder.completedAt = Date.now();
			c.state.completedCount++;

			// Broadcast event
			c.broadcast("reminderTriggered", reminder);

			console.log(`reminder triggered: ${reminder.message}`);
		},

		// Get all reminders
		getReminders: (c): Reminder[] => {
			return c.state.reminders;
		},

		// Cancel a pending scheduled reminder.
		cancelReminder: async (c, reminderId: string) => {
			const reminder = c.state.reminders.find((r) => r.id === reminderId);
			if (!reminder) return { success: false };

			const cancelled = await c.schedule.cancel(reminder.scheduleId);
			if (cancelled) {
				c.state.reminders = c.state.reminders.filter(
					(r) => r.id !== reminderId,
				);
			}

			return { success: cancelled };
		},

		// Get statistics about reminders
		getStats: (c) => {
			const total = c.state.reminders.length;
			const completed = c.state.completedCount;
			const pending = total - completed;

			return {
				total,
				completed,
				pending,
			};
		},
	},
});

export const registry = setup({
	use: { reminderActor },
});

export type Registry = typeof registry;
export type { DailyReport, Reminder };

registry.start();
  • c.cron.set stores a named recurring job on the Actor. 0 9 * * * is a standard five-field cron expression for 9:00 every day.
  • timezone takes an IANA name and defaults to UTC. The job keeps firing at 9:00 local time across daylight saving changes.
  • action names an action on the same Actor. The name is fixed in your code, never taken from a client.
  • name is unique per Actor. Calling cron.set again with the same name updates the job instead of adding a second one, so there is no need to re-arm a one-shot schedule after every run.
  • The Actor does not stay awake between runs. It can sleep as usual, and Rivet wakes it when the job is due.

onCreate runs once per Actor, so a new expression in your code only reaches Actors created after the deploy. To update Actors that already exist, call the same cron.set from onWake, which runs on every start. Calling it with an unchanged expression keeps the next run time as it is.

Build the report

The cron job calls sendDailyReport, an ordinary action:

import { actor, event, type ScheduledFireInfo, setup } from "rivetkit";

interface Reminder {
	id: string;
	scheduleId: string;
	message: string;
	scheduledAt: number;
	completedAt?: number;
}

interface DailyReport {
	from: number;
	to: number;
	completed: number;
	pending: number;
}

interface ReminderActorState {
	reminders: Reminder[];
	completedCount: number;
	lastReportAt: number;
	lastReport?: DailyReport;
}

const reminderActor = actor({
	state: {
		reminders: [] as Reminder[],
		completedCount: 0,
		lastReportAt: 0,
	} satisfies ReminderActorState as ReminderActorState,
	events: {
		reminderTriggered: event<Reminder>(),
		dailyReport: event<DailyReport>(),
	},

	// Install the daily report job when the Actor is created.
	onCreate: async (c) => {
		await c.cron.set({
			name: "daily-report",
			expression: "0 9 * * *",
			timezone: "America/Los_Angeles",
			action: "sendDailyReport",
		});
	},

	actions: {
		// Called by the daily-report cron job every day at 9:00.
		sendDailyReport: (c, fire: ScheduledFireInfo) => {
			const from = c.state.lastReportAt;
			const to = fire.scheduledAt;

			// Skip a run whose window was already reported.
			if (to <= from) return c.state.lastReport;

			const report: DailyReport = {
				from,
				to,
				completed: c.state.reminders.filter(
					(r) =>
						r.completedAt !== undefined &&
						r.completedAt >= from &&
						r.completedAt < to,
				).length,
				pending: c.state.reminders.filter(
					(r) => r.completedAt === undefined,
				).length,
			};

			c.state.lastReportAt = to;
			c.state.lastReport = report;
			c.broadcast("dailyReport", report);
			return report;
		},

		// Return the last report, the next run time, and recent runs.
		getDailyReport: async (c) => {
			const job = await c.cron.get("daily-report");
			const history = await c.cron.history("daily-report", { limit: 7 });
			return {
				lastReport: c.state.lastReport,
				nextRunAt: job?.nextRunAt,
				history,
			};
		},

		// Schedule a reminder with a delay in milliseconds
		scheduleReminder: async (c, message: string, delayMs: number) => {
			const id = `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
			const scheduleId = await c.schedule.after(
				delayMs,
				"triggerReminder",
				id,
			);
			const reminder: Reminder = {
				id,
				scheduleId,
				message,
				scheduledAt: Date.now() + delayMs,
			};

			c.state.reminders.push(reminder);

			return reminder;
		},

		// Schedule a reminder at a specific timestamp
		scheduleReminderAt: async (c, message: string, timestamp: number) => {
			const id = `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
			const scheduleId = await c.schedule.at(
				timestamp,
				"triggerReminder",
				id,
			);
			const reminder: Reminder = {
				id,
				scheduleId,
				message,
				scheduledAt: timestamp,
			};

			c.state.reminders.push(reminder);

			return reminder;
		},

		// Trigger a scheduled reminder
		triggerReminder: (c, reminderId: string) => {
			const reminder = c.state.reminders.find((r) => r.id === reminderId);
			if (!reminder) {
				console.warn(`reminder not found: ${reminderId}`);
				return;
			}

			// Mark as completed
			reminder.completedAt = Date.now();
			c.state.completedCount++;

			// Broadcast event
			c.broadcast("reminderTriggered", reminder);

			console.log(`reminder triggered: ${reminder.message}`);
		},

		// Get all reminders
		getReminders: (c): Reminder[] => {
			return c.state.reminders;
		},

		// Cancel a pending scheduled reminder.
		cancelReminder: async (c, reminderId: string) => {
			const reminder = c.state.reminders.find((r) => r.id === reminderId);
			if (!reminder) return { success: false };

			const cancelled = await c.schedule.cancel(reminder.scheduleId);
			if (cancelled) {
				c.state.reminders = c.state.reminders.filter(
					(r) => r.id !== reminderId,
				);
			}

			return { success: cancelled };
		},

		// Get statistics about reminders
		getStats: (c) => {
			const total = c.state.reminders.length;
			const completed = c.state.completedCount;
			const pending = total - completed;

			return {
				total,
				completed,
				pending,
			};
		},
	},
});

export const registry = setup({
	use: { reminderActor },
});

export type Registry = typeof registry;
export type { DailyReport, Reminder };

registry.start();
  • When a schedule calls an action, it appends a ScheduledFireInfo object after the action’s own arguments. It carries kind, id, name, scheduledAt, and firedAt.
  • The report covers the window from lastReportAt to fire.scheduledAt. Using the planned time instead of Date.now() keeps every window exactly one day long, even when a run starts a little late.
  • If the window was already reported, the action returns the saved report and changes nothing. Running it twice for the same occurrence is harmless, so a manual retry is safe.
  • It saves the report to c.state.lastReport and broadcasts dailyReport, so connected clients see it right away.
  • A run that is still going when the next one is due makes the next one skip. Failed runs are not retried; the job continues at its next normal time. See Execution behavior.

sendDailyReport is a public action, so any client that can reach the Actor can call it with any arguments. In production, check who connects with onBeforeConnect (see Authentication) and keep scheduled actions idempotent like this one.

Renaming or removing sendDailyReport deletes the daily-report job the next time it is due, because a recurring job is removed when its action no longer exists. Rename the action and the action field of cron.set in the same deploy, and call cron.set from onWake so existing Actors pick up the new name.

Check on the job

getDailyReport returns the last report, the job’s next run time, and its recent runs:

import { actor, event, type ScheduledFireInfo, setup } from "rivetkit";

interface Reminder {
	id: string;
	scheduleId: string;
	message: string;
	scheduledAt: number;
	completedAt?: number;
}

interface DailyReport {
	from: number;
	to: number;
	completed: number;
	pending: number;
}

interface ReminderActorState {
	reminders: Reminder[];
	completedCount: number;
	lastReportAt: number;
	lastReport?: DailyReport;
}

const reminderActor = actor({
	state: {
		reminders: [] as Reminder[],
		completedCount: 0,
		lastReportAt: 0,
	} satisfies ReminderActorState as ReminderActorState,
	events: {
		reminderTriggered: event<Reminder>(),
		dailyReport: event<DailyReport>(),
	},

	// Install the daily report job when the Actor is created.
	onCreate: async (c) => {
		await c.cron.set({
			name: "daily-report",
			expression: "0 9 * * *",
			timezone: "America/Los_Angeles",
			action: "sendDailyReport",
		});
	},

	actions: {
		// Called by the daily-report cron job every day at 9:00.
		sendDailyReport: (c, fire: ScheduledFireInfo) => {
			const from = c.state.lastReportAt;
			const to = fire.scheduledAt;

			// Skip a run whose window was already reported.
			if (to <= from) return c.state.lastReport;

			const report: DailyReport = {
				from,
				to,
				completed: c.state.reminders.filter(
					(r) =>
						r.completedAt !== undefined &&
						r.completedAt >= from &&
						r.completedAt < to,
				).length,
				pending: c.state.reminders.filter(
					(r) => r.completedAt === undefined,
				).length,
			};

			c.state.lastReportAt = to;
			c.state.lastReport = report;
			c.broadcast("dailyReport", report);
			return report;
		},

		// Return the last report, the next run time, and recent runs.
		getDailyReport: async (c) => {
			const job = await c.cron.get("daily-report");
			const history = await c.cron.history("daily-report", { limit: 7 });
			return {
				lastReport: c.state.lastReport,
				nextRunAt: job?.nextRunAt,
				history,
			};
		},

		// Schedule a reminder with a delay in milliseconds
		scheduleReminder: async (c, message: string, delayMs: number) => {
			const id = `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
			const scheduleId = await c.schedule.after(
				delayMs,
				"triggerReminder",
				id,
			);
			const reminder: Reminder = {
				id,
				scheduleId,
				message,
				scheduledAt: Date.now() + delayMs,
			};

			c.state.reminders.push(reminder);

			return reminder;
		},

		// Schedule a reminder at a specific timestamp
		scheduleReminderAt: async (c, message: string, timestamp: number) => {
			const id = `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
			const scheduleId = await c.schedule.at(
				timestamp,
				"triggerReminder",
				id,
			);
			const reminder: Reminder = {
				id,
				scheduleId,
				message,
				scheduledAt: timestamp,
			};

			c.state.reminders.push(reminder);

			return reminder;
		},

		// Trigger a scheduled reminder
		triggerReminder: (c, reminderId: string) => {
			const reminder = c.state.reminders.find((r) => r.id === reminderId);
			if (!reminder) {
				console.warn(`reminder not found: ${reminderId}`);
				return;
			}

			// Mark as completed
			reminder.completedAt = Date.now();
			c.state.completedCount++;

			// Broadcast event
			c.broadcast("reminderTriggered", reminder);

			console.log(`reminder triggered: ${reminder.message}`);
		},

		// Get all reminders
		getReminders: (c): Reminder[] => {
			return c.state.reminders;
		},

		// Cancel a pending scheduled reminder.
		cancelReminder: async (c, reminderId: string) => {
			const reminder = c.state.reminders.find((r) => r.id === reminderId);
			if (!reminder) return { success: false };

			const cancelled = await c.schedule.cancel(reminder.scheduleId);
			if (cancelled) {
				c.state.reminders = c.state.reminders.filter(
					(r) => r.id !== reminderId,
				);
			}

			return { success: cancelled };
		},

		// Get statistics about reminders
		getStats: (c) => {
			const total = c.state.reminders.length;
			const completed = c.state.completedCount;
			const pending = total - completed;

			return {
				total,
				completed,
				pending,
			};
		},
	},
});

export const registry = setup({
	use: { reminderActor },
});

export type Registry = typeof registry;
export type { DailyReport, Reminder };

registry.start();
  • c.cron.get returns the job’s settings and nextRunAt, the Unix time in milliseconds of the next run.
  • c.cron.history returns recent runs, newest first. Each entry has a result of running, ok, error, or skipped, plus the error when a run failed. Since failed runs are not retried, this is where you find them and build your own retry if you need one.
  • A job keeps 100 history entries by default. Pass maxHistory to cron.set to keep up to 1,000, or 0 to turn history off.

Add one-shot jobs, intervals, and cancellation

The reminders are one-shot schedules. Each one calls triggerReminder once and keeps the schedule ID so it can be cancelled:

import { actor, event, type ScheduledFireInfo, setup } from "rivetkit";

interface Reminder {
	id: string;
	scheduleId: string;
	message: string;
	scheduledAt: number;
	completedAt?: number;
}

interface DailyReport {
	from: number;
	to: number;
	completed: number;
	pending: number;
}

interface ReminderActorState {
	reminders: Reminder[];
	completedCount: number;
	lastReportAt: number;
	lastReport?: DailyReport;
}

const reminderActor = actor({
	state: {
		reminders: [] as Reminder[],
		completedCount: 0,
		lastReportAt: 0,
	} satisfies ReminderActorState as ReminderActorState,
	events: {
		reminderTriggered: event<Reminder>(),
		dailyReport: event<DailyReport>(),
	},

	// Install the daily report job when the Actor is created.
	onCreate: async (c) => {
		await c.cron.set({
			name: "daily-report",
			expression: "0 9 * * *",
			timezone: "America/Los_Angeles",
			action: "sendDailyReport",
		});
	},

	actions: {
		// Called by the daily-report cron job every day at 9:00.
		sendDailyReport: (c, fire: ScheduledFireInfo) => {
			const from = c.state.lastReportAt;
			const to = fire.scheduledAt;

			// Skip a run whose window was already reported.
			if (to <= from) return c.state.lastReport;

			const report: DailyReport = {
				from,
				to,
				completed: c.state.reminders.filter(
					(r) =>
						r.completedAt !== undefined &&
						r.completedAt >= from &&
						r.completedAt < to,
				).length,
				pending: c.state.reminders.filter(
					(r) => r.completedAt === undefined,
				).length,
			};

			c.state.lastReportAt = to;
			c.state.lastReport = report;
			c.broadcast("dailyReport", report);
			return report;
		},

		// Return the last report, the next run time, and recent runs.
		getDailyReport: async (c) => {
			const job = await c.cron.get("daily-report");
			const history = await c.cron.history("daily-report", { limit: 7 });
			return {
				lastReport: c.state.lastReport,
				nextRunAt: job?.nextRunAt,
				history,
			};
		},

		// Schedule a reminder with a delay in milliseconds
		scheduleReminder: async (c, message: string, delayMs: number) => {
			const id = `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
			const scheduleId = await c.schedule.after(
				delayMs,
				"triggerReminder",
				id,
			);
			const reminder: Reminder = {
				id,
				scheduleId,
				message,
				scheduledAt: Date.now() + delayMs,
			};

			c.state.reminders.push(reminder);

			return reminder;
		},

		// Schedule a reminder at a specific timestamp
		scheduleReminderAt: async (c, message: string, timestamp: number) => {
			const id = `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
			const scheduleId = await c.schedule.at(
				timestamp,
				"triggerReminder",
				id,
			);
			const reminder: Reminder = {
				id,
				scheduleId,
				message,
				scheduledAt: timestamp,
			};

			c.state.reminders.push(reminder);

			return reminder;
		},

		// Trigger a scheduled reminder
		triggerReminder: (c, reminderId: string) => {
			const reminder = c.state.reminders.find((r) => r.id === reminderId);
			if (!reminder) {
				console.warn(`reminder not found: ${reminderId}`);
				return;
			}

			// Mark as completed
			reminder.completedAt = Date.now();
			c.state.completedCount++;

			// Broadcast event
			c.broadcast("reminderTriggered", reminder);

			console.log(`reminder triggered: ${reminder.message}`);
		},

		// Get all reminders
		getReminders: (c): Reminder[] => {
			return c.state.reminders;
		},

		// Cancel a pending scheduled reminder.
		cancelReminder: async (c, reminderId: string) => {
			const reminder = c.state.reminders.find((r) => r.id === reminderId);
			if (!reminder) return { success: false };

			const cancelled = await c.schedule.cancel(reminder.scheduleId);
			if (cancelled) {
				c.state.reminders = c.state.reminders.filter(
					(r) => r.id !== reminderId,
				);
			}

			return { success: cancelled };
		},

		// Get statistics about reminders
		getStats: (c) => {
			const total = c.state.reminders.length;
			const completed = c.state.completedCount;
			const pending = total - completed;

			return {
				total,
				completed,
				pending,
			};
		},
	},
});

export const registry = setup({
	use: { reminderActor },
});

export type Registry = typeof registry;
export type { DailyReport, Reminder };

registry.start();
  • scheduleReminder uses c.schedule.after with a delay in milliseconds. scheduleReminderAt uses c.schedule.at with a Unix timestamp in milliseconds.
  • Both return a schedule ID, which the reminder stores as scheduleId. cancelReminder passes it to c.schedule.cancel, which returns false if the reminder already fired.
  • Like the cron job, the target action name triggerReminder is fixed in code, and the reminder ID is passed as its argument.

Pick the API by how often the work runs:

APIUse it for
c.schedule.after(delayMs, action, ...args)Run once after a delay. Returns an ID for c.schedule.cancel.
c.schedule.at(timestamp, action, ...args)Run once at a Unix timestamp in milliseconds. Returns an ID for c.schedule.cancel.
c.cron.set({ name, expression, action })Run on a calendar schedule, in a timezone.
c.cron.every({ name, interval, action })Run every interval milliseconds, at least 5,000. Use it for frequent work such as presence sweeps or cache refreshes. Runs stay on their planned times, so a slow run does not push the next one later.
c.cron.delete(name)Stop a recurring job. Recurring jobs are managed by name, and calling cron.set or cron.every again with the same name replaces the job.

See Schedule & Cron for c.schedule.list, c.cron.list, and the full set of options.

Try it

git clone https://github.com/rivet-dev/rivet.git
cd rivet/examples/scheduling
pnpm install
pnpm dev

Open http://localhost:5173. Schedule a reminder 5 seconds out and watch it move to completed when it fires. Schedule another and cancel it before it fires. The daily report runs at 9:00 Los Angeles time; getDailyReport shows its nextRunAt and, after the first run, the report and its history entry.

Next steps

  • Schedule & Cron: every schedule and cron API, run history, and execution behavior.
  • Lifecycle: when onCreate and onWake run, and how Actors sleep between scheduled runs.
  • Workflows: durable multi-step work for jobs that must finish even when a step fails.