Skip to main content

Basic Usage

📘 Doc-testing – Why do these examples look like tests?

This documentation uses testable code examples to ensure accuracy and reliability:

  • Guaranteed accuracy: All examples are real, working code that runs against the actual package(s)
  • Guaranteed latest comparisons: Further, our release script won't allow us to release a new version of Lumenize, without prompting us to update any doc-tested comparison package (e.g. Cap'n Web)
  • Always up-to-date: When a package changes, the tests fail and the docs must be updated
  • Copy-paste confidence: What you see is what works - no outdated or broken examples
  • Real-world patterns: Tests show complete, runnable scenarios, not just snippets

Ignore the test boilerplate (it(), describe(), etc.) - focus on the code inside.

The @cloudflare/actors alarms package solves a key limitation: Cloudflare only allows one native alarm per Durable Object instance. This package uses SQL storage to manage multiple scheduled tasks and ensures the single native alarm always fires for the next scheduled task.

This guide shows how to use the alarms package to schedule one-time, delayed, and recurring (cron) tasks in your Durable Objects.

Imports​

import { it, expect, vi } from 'vitest';
import { createTestingClient, type RpcAccessible } from '@lumenize/testing';
import { AlarmDO } from '../src';

Version​

This test asserts the installed version and our release script warns if we aren't using the latest version published to npm, so this living documentation should always be up to date.

import actorsPackage from '../node_modules/@cloudflare/actors/package.json';
it('detects package version', () => {
expect(actorsPackage.version).toBe('0.0.1-beta.6');
});

Installation​

npm install @cloudflare/actors

Setup​

To use the Alarms package, your Durable Object must:

  1. Extend from Actor (not DurableObject) - The Actor base class from @cloudflare/actors automatically initializes the alarms property and provides the setName() method required by the alarms system
  2. Implement the alarm() method - This delegates to the Alarms instance

Here's the complete Durable Object and Worker:

import { Actor } from "@cloudflare/actors";
import { type Schedule } from "@cloudflare/actors/alarms";

export class AlarmDO extends Actor<Env> {
executedAlarms: string[] = [];

// Required boilerplate: delegate to Alarms instance
async alarm() {
await this.alarms.alarm();
}

// Callback for alarms - gets called when an alarm fires
async handleAlarm(payload: any, schedule: Schedule) {
const message = `Alarm ${schedule.id} fired: ${JSON.stringify(payload)}`;
this.executedAlarms.push(message);
}

// Method to get executed alarms (for testing)
getExecutedAlarms(): string[] {
return this.executedAlarms;
}

// Method to clear executed alarms (for testing)
clearExecutedAlarms(): void {
this.executedAlarms = [];
}
}

// No default export needed - the test harness handles everything


The alarm() method is required boilerplate - Cloudflare's Durable Object API requires you to implement this handler method. The Alarms class can't automatically inject itself into that lifecycle hook, so you must explicitly delegate to it.

Important: The Actor base class automatically creates this.alarms for you - you don't need to manually instantiate it.

Scheduling Alarms​

The Alarms package supports three types of schedules:

  1. Date-based: Execute at a specific time
  2. Delay-based: Execute after N seconds
  3. Cron-based: Recurring execution using cron expressions
it('schedules multiple alarms with different types', async () => {
// createTestingClient provides direct RPC access to the DO
await using client = createTestingClient<RpcAccessible<InstanceType<typeof AlarmDO>>>(
'ALARM_DO',
'multi-types'
);

// Clear any previous test data
await client.clearExecutedAlarms();

// 1. Schedule with a Date (execute at specific time)
const futureDate = new Date(Date.now() + 500); // 500ms from now
const dateSchedule = await client.alarms.schedule(
futureDate,
'handleAlarm',
{ type: 'date', message: 'Executed at specific time' }
);
expect(dateSchedule.type).toBe('scheduled');
expect(dateSchedule.callback).toBe('handleAlarm');

// 2. Schedule with delay in seconds
const delaySchedule = await client.alarms.schedule(
1, // 1 second
'handleAlarm',
{ type: 'delay', message: 'Executed after delay' }
);
expect(delaySchedule.type).toBe('delayed');

// 3. Schedule with cron expression (every minute)
// Note: We won't wait for cron to fire - it would take 60 seconds
// Cron syntax reference: https://crontab.guru
const cronSchedule = await client.alarms.schedule(
'* * * * *',
'handleAlarm',
{ type: 'cron', message: 'Recurring task' }
);
expect(cronSchedule.type).toBe('cron');

// With @lumenize/testing, alarms fire automatically! Just wait for them.
await vi.waitFor(async () => {
const executed = await client.getExecutedAlarms();
expect(executed.length).toBeGreaterThanOrEqual(2);
});

// Verify both alarms executed with correct payloads
const executed = await client.getExecutedAlarms();
expect(executed.some((msg: string) => msg.includes('date'))).toBe(true);
expect(executed.some((msg: string) => msg.includes('delay'))).toBe(true);
});

Managing Scheduled Alarms​

You can query and cancel scheduled alarms:

it('queries and cancels scheduled alarms', async () => {
// createTestingClient provides direct RPC access to the DO
await using client = createTestingClient<RpcAccessible<InstanceType<typeof AlarmDO>>>(
'ALARM_DO',
'manage'
);

// Schedule several alarms
const schedule1 = await client.alarms.schedule(
10, // 10 seconds
'handleAlarm',
{ task: 'task-1' }
);

const schedule2 = await client.alarms.schedule(
20, // 20 seconds
'handleAlarm',
{ task: 'task-2' }
);

// Get all scheduled alarms
const allSchedules = await client.alarms.getSchedules();
expect(allSchedules.length).toBeGreaterThanOrEqual(2);

// Get a specific schedule by ID
const retrieved = await client.alarms.getSchedule(schedule1.id);
expect(retrieved?.payload).toEqual({ task: 'task-1' });

// Cancel a schedule
const cancelled = await client.alarms.cancelSchedule(schedule2.id);
expect(cancelled).toBe(true);

// Verify it's gone
const afterCancel = await client.alarms.getSchedules();
expect(afterCancel.some((s: any) => s.id === schedule2.id)).toBe(false);
});

Using Without Actor Base Class​

It's possible to use the Alarms package without extending Actor. See here for an example.

wrangler.jsonc​

{
"name": "actors-alarms-basic-usage",
"main": "test-harness.ts",
"compatibility_date": "2025-09-12",
"durable_objects": {
"bindings": [
{
"name": "ALARM_DO",
"class_name": "AlarmDO"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["AlarmDO"]
}
]
}


Try it out​

To run these tests:

vitest --run

For coverage reports:

vitest --run --coverage