<html lang="en">
<head></head>
<body>

<form id="mainForm" method="post" action="https://stackblitz.com/run" target="_self">
<input type="hidden" name="project[files][README.md]" value="# Scheduling

Demonstrates how to schedule tasks and execute code at specific times or intervals using Rivet Actors.

## Getting Started

```sh
git clone https://github.com/rivet-dev/rivet.git
cd rivet/examples/scheduling
npm install
npm run dev
```


## Features

- **Task scheduling**: Schedule actor actions to run at specific times with `schedule.at()`
- **Delayed execution**: Schedule tasks to run after a delay with `schedule.after()`
- **Persistent schedules**: Scheduled tasks survive actor restarts
- **Action callbacks**: Scheduled tasks invoke actor actions with custom payloads

## Implementation

This example demonstrates time-based task scheduling in Rivet Actors:

- **Actor Definition** ([`src/backend/registry.ts`](https://github.com/rivet-dev/rivet/tree/main/examples/scheduling/src/backend/registry.ts)): Shows how to use `schedule.at()` and `schedule.after()` to schedule future actions with persistent state

## Resources

Read more about [scheduling](/docs/actors/scheduling), [actions](/docs/actors/actions), and [state](/docs/actors/state).

## License

MIT
">
<input type="hidden" name="project[files][index.html]" value="&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;title&gt;Quickstart: Scheduling&lt;/title&gt;
    &lt;style&gt;
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, &#39;Segoe UI&#39;, Roboto, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }

        .app {
            max-width: 1200px;
            margin: 0 auto;
        }

        .header {
            text-align: center;
            color: white;
            margin-bottom: 30px;
        }

        .header h1 {
            font-size: 2.5rem;
            margin-bottom: 10px;
            text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
        }

        .header p {
            font-size: 1.1rem;
            opacity: 0.9;
        }

        .stats-bar {
            display: flex;
            gap: 15px;
            justify-content: center;
            margin-bottom: 30px;
            flex-wrap: wrap;
        }

        .stat {
            background: rgba(255, 255, 255, 0.95);
            padding: 15px 25px;
            border-radius: 12px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 5px;
        }

        .stat-label {
            font-size: 0.9rem;
            color: #666;
            font-weight: 500;
        }

        .stat-value {
            font-size: 1.8rem;
            font-weight: bold;
            color: #667eea;
        }

        .notifications {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 12px;
            padding: 20px;
            margin-bottom: 30px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }

        .notifications-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        }

        .notifications-header h3 {
            color: #333;
            font-size: 1.2rem;
        }

        .clear-btn {
            background: #f44336;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 6px;
            cursor: pointer;
            font-size: 0.9rem;
            font-weight: 500;
            transition: background 0.2s;
        }

        .clear-btn:hover {
            background: #d32f2f;
        }

        .notification {
            display: flex;
            gap: 15px;
            padding: 15px;
            background: #fff3cd;
            border-left: 4px solid #ffc107;
            border-radius: 8px;
            margin-bottom: 10px;
            animation: slideIn 0.3s ease-out;
        }

        @keyframes slideIn {
            from {
                transform: translateX(-20px);
                opacity: 0;
            }
            to {
                transform: translateX(0);
                opacity: 1;
            }
        }

        .notification-icon {
            font-size: 1.5rem;
        }

        .notification-content {
            flex: 1;
        }

        .notification-message {
            font-weight: 600;
            color: #333;
            margin-bottom: 5px;
        }

        .notification-time {
            font-size: 0.9rem;
            color: #666;
        }

        .content {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 30px;
        }

        @media (max-width: 768px) {
            .content {
                grid-template-columns: 1fr;
            }
        }

        .section {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 12px;
            padding: 25px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }

        .section h2 {
            color: #333;
            margin-bottom: 20px;
            font-size: 1.5rem;
        }

        .section h3 {
            color: #555;
            margin-bottom: 15px;
            font-size: 1.1rem;
        }

        .form-group {
            margin-bottom: 15px;
        }

        .form-group label {
            display: block;
            color: #555;
            margin-bottom: 8px;
            font-weight: 500;
        }

        .input {
            width: 100%;
            padding: 12px;
            border: 2px solid #e0e0e0;
            border-radius: 8px;
            font-size: 1rem;
            transition: border-color 0.2s;
        }

        .input:focus {
            outline: none;
            border-color: #667eea;
        }

        .schedule-options {
            display: grid;
            gap: 20px;
        }

        .option {
            padding: 20px;
            background: #f8f9fa;
            border-radius: 8px;
        }

        .btn {
            padding: 12px 24px;
            border: none;
            border-radius: 8px;
            font-size: 1rem;
            font-weight: 600;
            cursor: pointer;
            transition: all 0.2s;
            width: 100%;
        }

        .btn-primary {
            background: #667eea;
            color: white;
        }

        .btn-primary:hover:not(:disabled) {
            background: #5568d3;
            transform: translateY(-1px);
            box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3);
        }

        .btn-primary:disabled {
            background: #ccc;
            cursor: not-allowed;
        }

        .btn-cancel {
            background: #f44336;
            color: white;
            padding: 8px 16px;
            font-size: 0.9rem;
            width: auto;
        }

        .btn-cancel:hover {
            background: #d32f2f;
        }

        .empty-state {
            text-align: center;
            padding: 40px;
            color: #999;
            font-size: 1.1rem;
        }

        .reminders-list {
            display: flex;
            flex-direction: column;
            gap: 15px;
        }

        .reminder-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 15px;
            border-radius: 8px;
            transition: all 0.2s;
        }

        .reminder-item.pending {
            background: #e3f2fd;
            border-left: 4px solid #2196f3;
        }

        .reminder-item.completed {
            background: #e8f5e9;
            border-left: 4px solid #4caf50;
            opacity: 0.8;
        }

        .reminder-content {
            flex: 1;
        }

        .reminder-message {
            font-weight: 600;
            color: #333;
            margin-bottom: 8px;
            font-size: 1.05rem;
        }

        .reminder-meta {
            display: flex;
            gap: 15px;
            flex-wrap: wrap;
            font-size: 0.9rem;
        }

        .reminder-status {
            font-weight: 500;
        }

        .reminder-status.pending {
            color: #2196f3;
        }

        .reminder-status.completed {
            color: #4caf50;
        }

        .reminder-scheduled {
            color: #666;
        }

        .loading-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.8);
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 1.5rem;
            z-index: 1000;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div id=&quot;root&quot;&gt;&lt;/div&gt;
    &lt;script type=&quot;module&quot; src=&quot;/frontend/main.tsx&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
">
<input type="hidden" name="project[files][package.json]" value="{&quot;name&quot;:&quot;scheduling&quot;,&quot;version&quot;:&quot;2.0.21&quot;,&quot;private&quot;:true,&quot;type&quot;:&quot;module&quot;,&quot;scripts&quot;:{&quot;dev&quot;:&quot;vite&quot;,&quot;check-types&quot;:&quot;tsc --noEmit&quot;,&quot;test&quot;:&quot;vitest run&quot;,&quot;build&quot;:&quot;vite build &amp;&amp; vite build --mode server&quot;,&quot;start&quot;:&quot;srvx --static=public/ dist/server.js&quot;},&quot;devDependencies&quot;:{&quot;@types/node&quot;:&quot;^22.13.9&quot;,&quot;@types/react&quot;:&quot;^18.2.0&quot;,&quot;@types/react-dom&quot;:&quot;^18.2.0&quot;,&quot;@vitejs/plugin-react&quot;:&quot;^4.2.0&quot;,&quot;tsx&quot;:&quot;^3.12.7&quot;,&quot;typescript&quot;:&quot;^5.5.2&quot;,&quot;vite&quot;:&quot;^5.0.0&quot;,&quot;vitest&quot;:&quot;^3.1.1&quot;,&quot;vite-plugin-srvx&quot;:&quot;^1.0.0&quot;},&quot;dependencies&quot;:{&quot;@hono/node-server&quot;:&quot;^1.19.7&quot;,&quot;@hono/node-ws&quot;:&quot;^1.3.0&quot;,&quot;@rivetkit/react&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@b7a6ac3488d8d64f2cddf185312f05cf519f9413&quot;,&quot;hono&quot;:&quot;^4.0.0&quot;,&quot;react&quot;:&quot;^18.2.0&quot;,&quot;react-dom&quot;:&quot;^18.2.0&quot;,&quot;rivetkit&quot;:&quot;https://pkg.pr.new/rivet-dev/rivet/rivetkit@b7a6ac3488d8d64f2cddf185312f05cf519f9413&quot;,&quot;srvx&quot;:&quot;^0.10.0&quot;},&quot;template&quot;:{&quot;technologies&quot;:[&quot;typescript&quot;],&quot;tags&quot;:[],&quot;priority&quot;:1000,&quot;frontendPort&quot;:5173},&quot;license&quot;:&quot;MIT&quot;}">
<input type="hidden" name="project[files][tsconfig.json]" value="{
	&quot;compilerOptions&quot;: {
		&quot;target&quot;: &quot;esnext&quot;,
		&quot;lib&quot;: [&quot;esnext&quot;, &quot;dom&quot;],
		&quot;jsx&quot;: &quot;react-jsx&quot;,
		&quot;module&quot;: &quot;esnext&quot;,
		&quot;moduleResolution&quot;: &quot;bundler&quot;,
		&quot;types&quot;: [&quot;node&quot;],
		&quot;resolveJsonModule&quot;: true,
		&quot;allowJs&quot;: true,
		&quot;checkJs&quot;: false,
		&quot;noEmit&quot;: true,
		&quot;isolatedModules&quot;: true,
		&quot;allowSyntheticDefaultImports&quot;: true,
		&quot;forceConsistentCasingInFileNames&quot;: true,
		&quot;strict&quot;: true,
		&quot;skipLibCheck&quot;: true,
		&quot;allowImportingTsExtensions&quot;: true,
		&quot;rewriteRelativeImportExtensions&quot;: true
	},
	&quot;include&quot;: [&quot;src/**/*.ts&quot;, &quot;src/**/*.tsx&quot;, &quot;frontend/**/*.tsx&quot;, &quot;tests/**/*.ts&quot;]
}
">
<input type="hidden" name="project[files][turbo.json]" value="{
	&quot;$schema&quot;: &quot;https://turbo.build/schema.json&quot;,
	&quot;extends&quot;: [&quot;//&quot;],
	&quot;tasks&quot;: {
		&quot;build&quot;: {
			&quot;dependsOn&quot;: [&quot;@rivetkit/react#build&quot;, &quot;rivetkit#build&quot;]
		}
	}
}
">
<input type="hidden" name="project[files][vercel.json]" value="{
	&quot;framework&quot;: &quot;hono&quot;
}
">
<input type="hidden" name="project[files][vite.config.ts]" value="import { defineConfig } from &quot;vite&quot;;
import react from &quot;@vitejs/plugin-react&quot;;
import srvx from &quot;vite-plugin-srvx&quot;;

export default defineConfig({
	plugins: [react(), ...srvx({ entry: &quot;src/server.ts&quot; })],
});
">
<input type="hidden" name="project[files][vitest.config.ts]" value="import { defineConfig } from &quot;vitest/config&quot;;

export default defineConfig({
	test: {
		include: [&quot;tests/**/*.test.ts&quot;],
	},
});
">
<input type="hidden" name="project[files][frontend/App.tsx]" value="import { createRivetKit } from &quot;@rivetkit/react&quot;;
import { useEffect, useState } from &quot;react&quot;;
import type { Reminder, Registry } from &quot;../src/actors.ts&quot;;

const { useActor } = createRivetKit&lt;Registry&gt;(`${window.location.origin}/api/rivet`);

export function App() {
	const [reminders, setReminders] = useState&lt;Reminder[]&gt;([]);
	const [message, setMessage] = useState(&quot;&quot;);
	const [delay, setDelay] = useState(5);
	const [timestamp, setTimestamp] = useState(&quot;&quot;);
	const [triggeredReminders, setTriggeredReminders] = useState&lt;Reminder[]&gt;([]);
	const [stats, setStats] = useState({ total: 0, completed: 0, pending: 0 });

	const reminderActor = useActor({
		name: &quot;reminderActor&quot;,
		key: [&quot;main&quot;],
	});

	// Load initial state
	useEffect(() =&gt; {
		if (reminderActor.connection) {
			reminderActor.connection.getReminders().then((initialReminders) =&gt; {
				setReminders(initialReminders);
			}).catch((error) =&gt; {
				console.error(&quot;error loading reminders&quot;, error);
			});

			reminderActor.connection.getStats().then((initialStats) =&gt; {
				setStats(initialStats);
			}).catch((error) =&gt; {
				console.error(&quot;error loading stats&quot;, error);
			});
		}
	}, [reminderActor.connection]);

	// Listen for reminder triggered events
	reminderActor.useEvent(&quot;reminderTriggered&quot;, (reminder: Reminder) =&gt; {
		// Update the reminders list
		setReminders((prev) =&gt;
			prev.map((r) =&gt; (r.id === reminder.id ? reminder : r))
		);

		// Add to triggered notifications
		setTriggeredReminders((prev) =&gt; [reminder, ...prev].slice(0, 5));

		// Update stats
		if (reminderActor.connection) {
			reminderActor.connection.getStats().then(setStats).catch((error) =&gt; {
				console.error(&quot;error loading stats&quot;, error);
			});
		}
	});

	// Schedule a reminder with delay
	const handleScheduleReminder = async () =&gt; {
		if (!reminderActor.connection || !message.trim()) return;

		const delayMs = delay * 1000;
		const reminder = await reminderActor.connection.scheduleReminder(message, delayMs);
		setReminders((prev) =&gt; [...prev, reminder]);
		setMessage(&quot;&quot;);

		// Update stats
		const newStats = await reminderActor.connection.getStats();
		setStats(newStats);
	};

	// Schedule a reminder at a specific time
	const handleScheduleAt = async () =&gt; {
		if (!reminderActor.connection || !message.trim() || !timestamp) return;

		const timestampMs = new Date(timestamp).getTime();
		const reminder = await reminderActor.connection.scheduleReminderAt(message, timestampMs);
		setReminders((prev) =&gt; [...prev, reminder]);
		setMessage(&quot;&quot;);
		setTimestamp(&quot;&quot;);

		// Update stats
		const newStats = await reminderActor.connection.getStats();
		setStats(newStats);
	};

	// Cancel a reminder
	const handleCancelReminder = async (reminderId: string) =&gt; {
		if (!reminderActor.connection) return;

		await reminderActor.connection.cancelReminder(reminderId);
		setReminders((prev) =&gt; prev.filter((r) =&gt; r.id !== reminderId));

		// Update stats
		const newStats = await reminderActor.connection.getStats();
		setStats(newStats);
	};

	// Calculate time until reminder triggers
	const getTimeUntil = (scheduledAt: number) =&gt; {
		const now = Date.now();
		const diff = scheduledAt - now;

		if (diff &lt;= 0) return &quot;now&quot;;

		const seconds = Math.floor(diff / 1000);
		const minutes = Math.floor(seconds / 60);
		const hours = Math.floor(minutes / 60);

		if (hours &gt; 0) return `in ${hours}h ${minutes % 60}m`;
		if (minutes &gt; 0) return `in ${minutes}m ${seconds % 60}s`;
		return `in ${seconds}s`;
	};

	// Clear triggered notifications
	const clearTriggered = () =&gt; {
		setTriggeredReminders([]);
	};

	return (
		&lt;div className=&quot;app&quot;&gt;
			&lt;div className=&quot;header&quot;&gt;
				&lt;h1&gt;Quickstart: Scheduling&lt;/h1&gt;
				&lt;p&gt;Demonstrating c.schedule.after() and c.schedule.at()&lt;/p&gt;
			&lt;/div&gt;

			&lt;div className=&quot;stats-bar&quot;&gt;
				&lt;div className=&quot;stat&quot;&gt;
					&lt;span className=&quot;stat-label&quot;&gt;Total:&lt;/span&gt;
					&lt;span className=&quot;stat-value&quot;&gt;{stats.total}&lt;/span&gt;
				&lt;/div&gt;
				&lt;div className=&quot;stat&quot;&gt;
					&lt;span className=&quot;stat-label&quot;&gt;Completed:&lt;/span&gt;
					&lt;span className=&quot;stat-value&quot;&gt;{stats.completed}&lt;/span&gt;
				&lt;/div&gt;
				&lt;div className=&quot;stat&quot;&gt;
					&lt;span className=&quot;stat-label&quot;&gt;Pending:&lt;/span&gt;
					&lt;span className=&quot;stat-value&quot;&gt;{stats.pending}&lt;/span&gt;
				&lt;/div&gt;
			&lt;/div&gt;

			{triggeredReminders.length &gt; 0 &amp;&amp; (
				&lt;div className=&quot;notifications&quot;&gt;
					&lt;div className=&quot;notifications-header&quot;&gt;
						&lt;h3&gt;Recent Notifications&lt;/h3&gt;
						&lt;button onClick={clearTriggered} className=&quot;clear-btn&quot;&gt;Clear&lt;/button&gt;
					&lt;/div&gt;
					{triggeredReminders.map((reminder) =&gt; (
						&lt;div key={reminder.id} className=&quot;notification&quot;&gt;
							&lt;div className=&quot;notification-icon&quot;&gt;🔔&lt;/div&gt;
							&lt;div className=&quot;notification-content&quot;&gt;
								&lt;div className=&quot;notification-message&quot;&gt;{reminder.message}&lt;/div&gt;
								&lt;div className=&quot;notification-time&quot;&gt;
									Triggered at {new Date(reminder.completedAt!).toLocaleTimeString()}
								&lt;/div&gt;
							&lt;/div&gt;
						&lt;/div&gt;
					))}
				&lt;/div&gt;
			)}

			&lt;div className=&quot;content&quot;&gt;
				&lt;div className=&quot;section&quot;&gt;
					&lt;h2&gt;Schedule Reminder&lt;/h2&gt;

					&lt;div className=&quot;form-group&quot;&gt;
						&lt;label&gt;Message:&lt;/label&gt;
						&lt;input
							type=&quot;text&quot;
							value={message}
							onChange={(e) =&gt; setMessage(e.currentTarget.value)}
							placeholder=&quot;Enter reminder message&quot;
							className=&quot;input&quot;
						/&gt;
					&lt;/div&gt;

					&lt;div className=&quot;schedule-options&quot;&gt;
						&lt;div className=&quot;option&quot;&gt;
							&lt;h3&gt;After Delay&lt;/h3&gt;
							&lt;div className=&quot;form-group&quot;&gt;
								&lt;label&gt;Delay (seconds):&lt;/label&gt;
								&lt;input
									type=&quot;number&quot;
									value={delay}
									onChange={(e) =&gt; setDelay(Number(e.currentTarget.value))}
									min=&quot;1&quot;
									className=&quot;input&quot;
								/&gt;
							&lt;/div&gt;
							&lt;button
								onClick={handleScheduleReminder}
								disabled={!message.trim()}
								className=&quot;btn btn-primary&quot;
							&gt;
								Schedule Reminder
							&lt;/button&gt;
						&lt;/div&gt;

						&lt;div className=&quot;option&quot;&gt;
							&lt;h3&gt;At Specific Time&lt;/h3&gt;
							&lt;div className=&quot;form-group&quot;&gt;
								&lt;label&gt;Date &amp; Time:&lt;/label&gt;
								&lt;input
									type=&quot;datetime-local&quot;
									value={timestamp}
									onChange={(e) =&gt; setTimestamp(e.currentTarget.value)}
									className=&quot;input&quot;
								/&gt;
							&lt;/div&gt;
							&lt;button
								onClick={handleScheduleAt}
								disabled={!message.trim() || !timestamp}
								className=&quot;btn btn-primary&quot;
							&gt;
								Schedule at Time
							&lt;/button&gt;
						&lt;/div&gt;
					&lt;/div&gt;
				&lt;/div&gt;

				&lt;div className=&quot;section&quot;&gt;
					&lt;h2&gt;Reminders&lt;/h2&gt;
					{reminders.length === 0 ? (
						&lt;div className=&quot;empty-state&quot;&gt;No reminders scheduled&lt;/div&gt;
					) : (
						&lt;div className=&quot;reminders-list&quot;&gt;
							{reminders.map((reminder) =&gt; (
								&lt;div
									key={reminder.id}
									className={`reminder-item ${reminder.completedAt ? &#39;completed&#39; : &#39;pending&#39;}`}
								&gt;
									&lt;div className=&quot;reminder-content&quot;&gt;
										&lt;div className=&quot;reminder-message&quot;&gt;{reminder.message}&lt;/div&gt;
										&lt;div className=&quot;reminder-meta&quot;&gt;
											{reminder.completedAt ? (
												&lt;span className=&quot;reminder-status completed&quot;&gt;
													✓ Completed at {new Date(reminder.completedAt).toLocaleString()}
												&lt;/span&gt;
											) : (
												&lt;&gt;
													&lt;span className=&quot;reminder-status pending&quot;&gt;
														{getTimeUntil(reminder.scheduledAt)}
													&lt;/span&gt;
													&lt;span className=&quot;reminder-scheduled&quot;&gt;
														Scheduled: {new Date(reminder.scheduledAt).toLocaleString()}
													&lt;/span&gt;
												&lt;/&gt;
											)}
										&lt;/div&gt;
									&lt;/div&gt;
									{!reminder.completedAt &amp;&amp; (
										&lt;button
											onClick={() =&gt; handleCancelReminder(reminder.id)}
											className=&quot;btn btn-cancel&quot;
										&gt;
											Cancel
										&lt;/button&gt;
									)}
								&lt;/div&gt;
							))}
						&lt;/div&gt;
					)}
				&lt;/div&gt;
			&lt;/div&gt;

			{!reminderActor.connection &amp;&amp; (
				&lt;div className=&quot;loading-overlay&quot;&gt;Connecting to server...&lt;/div&gt;
			)}
		&lt;/div&gt;
	);
}
">
<input type="hidden" name="project[files][frontend/main.tsx]" value="import { StrictMode } from &quot;react&quot;;
import { createRoot } from &quot;react-dom/client&quot;;
import { App } from &quot;./App.tsx&quot;;

const root = document.getElementById(&quot;root&quot;);
if (!root) throw new Error(&quot;Root element not found&quot;);

createRoot(root).render(
	&lt;StrictMode&gt;
		&lt;App /&gt;
	&lt;/StrictMode&gt;
);
">
<input type="hidden" name="project[files][tests/scheduling.test.ts]" value="import { setupTest } from &quot;rivetkit/test&quot;;
import { describe, expect, test } from &quot;vitest&quot;;
import { registry } from &quot;../src/actors.ts&quot;;

// Helper to wait for a delay
const wait = (ms: number) =&gt; new Promise((resolve) =&gt; setTimeout(resolve, ms));

describe(&quot;reminder scheduling&quot;, () =&gt; {
	test(&quot;triggers reminder after delay&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);
		const reminderClient =
			client.reminderActor.getOrCreate(&quot;test-after-delay&quot;);
		const reminder = await reminderClient.scheduleReminder(
			&quot;Test reminder&quot;,
			100,
		);

		// Verify reminder was created
		expect(reminder.id).toBeDefined();
		expect(reminder.message).toBe(&quot;Test reminder&quot;);
		expect(reminder.completedAt).toBeUndefined();

		// Wait for the scheduled action to execute
		await wait(150);

		const reminders = await reminderClient.getReminders();
		const completed = reminders.find((r) =&gt; r.id === reminder.id);

		expect(completed?.completedAt).toBeDefined();
	});

	test(&quot;schedules reminder at specific timestamp&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);
		const reminderClient =
			client.reminderActor.getOrCreate(&quot;test-at-timestamp&quot;);
		const futureTime = Date.now() + 100;
		const reminder = await reminderClient.scheduleReminderAt(
			&quot;Future reminder&quot;,
			futureTime,
		);

		// Verify reminder was created
		expect(reminder.scheduledAt).toBe(futureTime);
		expect(reminder.completedAt).toBeUndefined();

		// Wait for the scheduled time
		await wait(150);

		const reminders = await reminderClient.getReminders();
		const completed = reminders.find((r) =&gt; r.id === reminder.id);

		expect(completed?.completedAt).toBeDefined();
	});

	test(&quot;passes correct arguments to scheduled actions&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);
		const reminderClient =
			client.reminderActor.getOrCreate(&quot;test-arguments&quot;);

		// Schedule multiple reminders with different IDs
		const reminder1 = await reminderClient.scheduleReminder(
			&quot;First reminder&quot;,
			50,
		);
		const reminder2 = await reminderClient.scheduleReminder(
			&quot;Second reminder&quot;,
			100,
		);
		const reminder3 = await reminderClient.scheduleReminder(
			&quot;Third reminder&quot;,
			150,
		);

		// Wait for all to trigger
		await wait(200);

		const reminders = await reminderClient.getReminders();

		// Verify each reminder received the correct ID and was triggered
		const completed1 = reminders.find((r) =&gt; r.id === reminder1.id);
		const completed2 = reminders.find((r) =&gt; r.id === reminder2.id);
		const completed3 = reminders.find((r) =&gt; r.id === reminder3.id);

		expect(completed1?.completedAt).toBeDefined();
		expect(completed2?.completedAt).toBeDefined();
		expect(completed3?.completedAt).toBeDefined();
	});

	test(&quot;cancels scheduled reminder&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);
		const reminderClient = client.reminderActor.getOrCreate(&quot;test-cancel&quot;);
		const reminder = await reminderClient.scheduleReminder(
			&quot;To be cancelled&quot;,
			100,
		);

		// Cancel the reminder before it triggers
		await reminderClient.cancelReminder(reminder.id);

		// Wait past the original trigger time
		await wait(150);

		const reminders = await reminderClient.getReminders();
		const found = reminders.find((r) =&gt; r.id === reminder.id);

		// Reminder should not exist (was removed from state)
		expect(found).toBeUndefined();
	});

	test(&quot;triggers multiple reminders in correct order&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);
		const reminderClient =
			client.reminderActor.getOrCreate(&quot;test-multiple&quot;);

		// Schedule 5 reminders with different delays
		const reminder1 = await reminderClient.scheduleReminder(
			&quot;Reminder 1&quot;,
			50,
		);
		const reminder2 = await reminderClient.scheduleReminder(
			&quot;Reminder 2&quot;,
			100,
		);
		const reminder3 = await reminderClient.scheduleReminder(
			&quot;Reminder 3&quot;,
			150,
		);
		const reminder4 = await reminderClient.scheduleReminder(
			&quot;Reminder 4&quot;,
			200,
		);
		const reminder5 = await reminderClient.scheduleReminder(
			&quot;Reminder 5&quot;,
			250,
		);

		// Wait for all to complete
		await wait(300);

		const reminders = await reminderClient.getReminders();

		// Verify all are completed
		expect(
			reminders.find((r) =&gt; r.id === reminder1.id)?.completedAt,
		).toBeDefined();
		expect(
			reminders.find((r) =&gt; r.id === reminder2.id)?.completedAt,
		).toBeDefined();
		expect(
			reminders.find((r) =&gt; r.id === reminder3.id)?.completedAt,
		).toBeDefined();
		expect(
			reminders.find((r) =&gt; r.id === reminder4.id)?.completedAt,
		).toBeDefined();
		expect(
			reminders.find((r) =&gt; r.id === reminder5.id)?.completedAt,
		).toBeDefined();

		const stats = await reminderClient.getStats();
		expect(stats.completed).toBe(5);
	});

	// Skipping restart test as direct actor access is not available in setupTest
	test.skip(&quot;scheduled actions persist across actor restarts&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);
		const reminderClient = client.reminderActor.getOrCreate(&quot;test-restart&quot;);
		const reminder = await reminderClient.scheduleReminder(
			&quot;Persistent reminder&quot;,
			100,
		);

		// This test would require direct access to the actor instance to stop it
		// which is not provided by setupTest

		await wait(150);

		const reminders = await reminderClient.getReminders();
		const completed = reminders.find((r) =&gt; r.id === reminder.id);

		expect(completed?.completedAt).toBeDefined();
	});

	test(&quot;updates stats correctly&quot;, async (ctx) =&gt; {
		const { client } = await setupTest(ctx, registry);
		const reminderClient = client.reminderActor.getOrCreate(&quot;test-stats&quot;);

		let stats = await reminderClient.getStats();
		expect(stats.total).toBe(0);
		expect(stats.completed).toBe(0);
		expect(stats.pending).toBe(0);

		// Schedule some reminders
		await reminderClient.scheduleReminder(&quot;Reminder 1&quot;, 50);
		await reminderClient.scheduleReminder(&quot;Reminder 2&quot;, 100);
		await reminderClient.scheduleReminder(&quot;Reminder 3&quot;, 150);

		stats = await reminderClient.getStats();
		expect(stats.total).toBe(3);
		expect(stats.pending).toBe(3);
		expect(stats.completed).toBe(0);

		// Wait for first reminder to trigger
		await wait(70);

		stats = await reminderClient.getStats();
		expect(stats.total).toBe(3);
		expect(stats.pending).toBe(2);
		expect(stats.completed).toBe(1);

		// Wait for all remaining
		await wait(100);

		stats = await reminderClient.getStats();
		expect(stats.total).toBe(3);
		expect(stats.pending).toBe(0);
		expect(stats.completed).toBe(3);
	});
});
">
<input type="hidden" name="project[files][src/actors.ts]" value="import { actor, setup } from &quot;rivetkit&quot;;

interface Reminder {
	id: string;
	message: string;
	scheduledAt: number;
	completedAt?: number;
}

interface ReminderActorState {
	reminders: Reminder[];
	completedCount: number;
}

const reminderActor = actor({
	state: {
		reminders: [] as Reminder[],
		completedCount: 0,
	} satisfies ReminderActorState as ReminderActorState,

	actions: {
		// Schedule a reminder with a delay in milliseconds
		scheduleReminder: (c, message: string, delayMs: number) =&gt; {
			const reminder: Reminder = {
				id: `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
				message,
				scheduledAt: Date.now() + delayMs,
			};

			c.state.reminders.push(reminder);
			c.schedule.after(delayMs, &quot;triggerReminder&quot;, reminder.id);

			return reminder;
		},

		// Schedule a reminder at a specific timestamp
		scheduleReminderAt: (c, message: string, timestamp: number) =&gt; {
			const reminder: Reminder = {
				id: `reminder-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
				message,
				scheduledAt: timestamp,
			};

			c.state.reminders.push(reminder);
			c.schedule.at(timestamp, &quot;triggerReminder&quot;, reminder.id);

			return reminder;
		},

		// Trigger a scheduled reminder
		triggerReminder: (c, reminderId: string) =&gt; {
			const reminder = c.state.reminders.find((r) =&gt; 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(&quot;reminderTriggered&quot;, reminder);

			console.log(`reminder triggered: ${reminder.message}`);
		},

		// Get all reminders
		getReminders: (c): Reminder[] =&gt; {
			return c.state.reminders;
		},

		// Cancel a scheduled reminder
		// Note: Rivet doesn&#39;t currently support canceling scheduled actions
		// This will only remove the reminder from state
		cancelReminder: (c, reminderId: string) =&gt; {
			// Remove from state
			c.state.reminders = c.state.reminders.filter(
				(r) =&gt; r.id !== reminderId,
			);

			return {
				success: true,
				message:
					&quot;reminder removed from state (note: scheduled action may still fire)&quot;,
			};
		},

		// Get statistics about reminders
		getStats: (c) =&gt; {
			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 { Reminder };
">
<input type="hidden" name="project[files][src/server.ts]" value="import { Hono } from &quot;hono&quot;;
import { registry } from &quot;./actors.ts&quot;;

const app = new Hono();
app.all(&quot;/api/rivet/*&quot;, (c) =&gt; registry.handler(c.req.raw));
export default app;
">
<input type="hidden" name="project[description]" value="generated by https://pkg.pr.new">
<input type="hidden" name="project[template]" value="node">
<input type="hidden" name="project[title]" value="scheduling">
</form>
<script>document.getElementById("mainForm").submit();</script>

</body></html>