Skip to content

Instantly share code, notes, and snippets.

@NuroDev
Created November 21, 2025 16:36
Show Gist options
  • Select an option

  • Save NuroDev/99023c80ff9344d45524add6226bbd1c to your computer and use it in GitHub Desktop.

Select an option

Save NuroDev/99023c80ff9344d45524add6226bbd1c to your computer and use it in GitHub Desktop.
⏲️ Schedule Controller - A minimal `Hono` style controller for Cloudflare Workers scheduled handler
export interface ScheduleHandlerContext<E = Env> {
/**
* The scheduled event controller providing details about the cron trigger.
*/
controller: ScheduledController;
/**
* The environment bindings available to the worker.
*/
env: E;
/**
* The execution context for managing the lifetime of asynchronous tasks.
*/
executionCtx: ExecutionContext;
}
export type ScheduleHandler<E = Env> = (
context: ScheduleHandlerContext<E>,
) => void | Promise<void>;
/**
* Creates a type-safe scheduled task handler with proper type inference.
*
* This is a helper function that provides better IDE autocomplete and type checking
* when defining scheduled task handlers. It's an identity function that returns the
* handler unchanged but with explicit type information.
*
* @template E - The environment bindings type (defaults to Env)
*
* @param handler - The scheduled task handler function to wrap
*
* @returns The same handler with explicit ScheduleHandler type
*
* @example
* ```ts
* const handler = createTaskHandler(async (c) => {
* console.info(`Running cron: ${c.controller.cron}`);
* // ...
* });
*
* // Register with schedule controller
* app.task('* /30 * * * *', handler);
* ```
*
* @example
* ```ts
* // With custom environment type
* interface CustomEnv extends Env {
* MY_KV: KVNamespace;
* }
*
* const customHandler = createTaskHandler<CustomEnv>(async (context) => {
* const value = await context.env.MY_KV.get('key');
* console.info(`Got value: ${value}`);
* });
* ```
*/
export const createTaskHandler = <E = Env>(
handler: ScheduleHandler<E>,
): ScheduleHandler<E> => handler;
/**
* Controller for managing and executing scheduled tasks in Cloudflare Workers.
*
* This class provides a fluent API for registering cron job handlers and automatically
* matches them to the appropriate cron schedule at runtime. Multiple handlers can be
* registered for the same schedule and will be executed in parallel.
*
* @template E - The environment bindings type (defaults to Env)
*
* @example
* ```ts
* // Create a schedule controller
* const app = new ScheduleController<Env>();
*
* // Register handlers for different schedules
* app.task('* /30 * * * *', createTaskHandler(async (context) => {
* console.info('Running every 30 minutes');
* // ...
* }));
* app.task('0 0 * * *', createTaskHandler(async (context) => {
* console.info('Running daily at midnight');
* // ...
* }));
*
* export default {
* scheduled: app.scheduled
* };
* ```
*
* @example
* ```ts
* // Multiple handlers for the same schedule run in parallel
* app
* .task('* /30 * * * *', sendAnalyticsHandler)
* .task('* /30 * * * *', pruneCacheHandler);
* ```
*/
export class ScheduleController<E = Env> {
private _jobs: Map<string, Array<ScheduleHandler<E>>> = new Map();
/**
* Registers a scheduled task handler for a specific cron schedule.
*
* Multiple handlers can be registered for the same schedule - they will all
* execute in parallel when the cron triggers.
*
* @param schedule - Cron expression (e.g., '* /30 * * * *' for every 30 minutes)
* @param handler - The handler function to execute when the cron triggers
*
* @returns The ScheduleController instance for method chaining
*
* @example
* ```ts
* app.task('0 * * * *', createTaskHandler(async (context) => {
* const { env, controller } = context;
* console.info(`Hourly task triggered at ${controller.scheduledTime}`);
* await env.WORKFLOWS.create({ id: 'hourly-workflow' });
* }));
* ```
*/
task(schedule: string, handler: ScheduleHandler<E>): this {
const existingHandlers = this._jobs.get(schedule) ?? [];
this._jobs.set(schedule, existingHandlers.concat(handler));
return this;
}
/**
* Gets the Cloudflare Workers scheduled event handler.
*
* This getter returns a function compatible with Cloudflare Workers'
* `ExportedHandlerScheduledHandler` interface. When a cron trigger fires,
* it matches the cron expression to registered handlers and executes all
* matching handlers in parallel.
*
* If no handlers match the triggered cron schedule, a warning is logged.
*
* @returns A scheduled event handler for Cloudflare Workers
*
* @example
* ```ts
* const app = new ScheduleController();
*
* app.task('* /30 * * * *', () => {
* console.info('Running every 30 minutes');
* });
*
* export default {
* scheduled: app.scheduled
* } satisfies ExportedHandler;
* ```
*/
get scheduled(): ExportedHandlerScheduledHandler<E> {
return async (controller, env, executionCtx): Promise<void> => {
const jobs = this._jobs.get(controller.cron) ?? [];
if (!jobs.length)
return console.warn(
`No cron job found for schedule "${controller.cron}"`,
);
await Promise.all(
jobs.map((handler) =>
handler({
controller,
env,
executionCtx,
}),
),
);
};
}
}
@NuroDev
Copy link
Author

NuroDev commented Nov 21, 2025

🦄 Example

// src/index.ts
import { createTaskHandler, ScheduleController } from 'path/to/schedule-controller';

// Create a task with some logic you want to run.
const sendEmailsTask = createTaskHandler(async (c): Promise<void> => {
	console.info('Scheduled: Sending email digests to users');
	c.controller.cron; // 0 8 * * *

	// ... email sending logic here ...
});

const app = new ScheduleController();

// Register the task to run every day at 8 AM
app.task('0 8 * * *', sendEmailsTask);

// Or create the task handler inline
app.task('*/30 * * * *', () => {
	console.log('Scheduled: Running task every 30 minutes');
});

export default {
	scheduled: app.scheduled,
} satisfies ExportedHandler<Env>;
// wrangler.jsonc
{
	"$schema": "node_modules/wrangler/config-schema.json",
	"name": "example-scheduled-worker",
	"main": "src/index.ts",
	"compatibility_date": "2025-11-09",
	"triggers": {
		"crons": [
			"0 8 * * *",
			"*/30 * * * *"
		]
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment