Skip to content

Instantly share code, notes, and snippets.

@NuroDev
Last active October 19, 2025 23:15
Show Gist options
  • Save NuroDev/4b9651170af37f0f7ee22d93a0f88042 to your computer and use it in GitHub Desktop.
Save NuroDev/4b9651170af37f0f7ee22d93a0f88042 to your computer and use it in GitHub Desktop.
⏲️ Schedule Controller - A minimal `Hono` style controller for Cloudflare Workers `scheduled`
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "example-scheduled-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-01",
"triggers": {
"crons": [
"0 8 * * *",
"*/30 * * * *"
]
}
}
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 scheduled = new ScheduleController()
// Register the task to run every day at 8 AM
.handler('0 8 * * *', sendEmailsTask)
// Or create the task handler inline
.handler('*/30 * * * *', () => {
console.log('Scheduled: Running task every 30 minutes');
});
export default {
scheduled: scheduled.process,
} satisfies ExportedHandler<Env>;
export type ScheduleHandler<E = Env> = (
context: ExecutionContext & {
controller: ScheduledController;
env: 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
* scheduleController.handler('* /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 scheduleController = new ScheduleController<Env>();
*
* // Register handlers for different schedules
* scheduleController
* .handler('* /30 * * * *', createTaskHandler(async (context) => {
* console.info('Running every 30 minutes');
* // ...
* }))
* .handler('0 0 * * *', createTaskHandler(async (context) => {
* console.info('Running daily at midnight');
* // ...
* }));
*
* // Export the processor in your worker
* export default {
* scheduled: scheduleController.process
* };
* ```
*
* @example
* ```ts
* // Multiple handlers for the same schedule run in parallel
* scheduleController
* .handler('* /30 * * * *', sendAnalyticsHandler)
* .handler('* /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
* scheduleController.handler('0 * * * *', createTaskHandler(async (context) => {
* const { env, controller } = context;
* console.info(`Hourly task triggered at ${controller.scheduledTime}`);
* await env.WORKFLOWS.create({ id: 'hourly-workflow' });
* }));
* ```
*/
handler = (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
* // In your worker's main file (src/index.ts)
* const scheduleController = new ScheduleController();
*
* // Register handlers...
* scheduleController.handler('* /30 * * * *', myHandler);
*
* // Export for Cloudflare Workers
* export default {
* fetch: app.fetch,
* scheduled: scheduleController.process
* };
* ```
*/
get process(): ExportedHandlerScheduledHandler<E> {
return async (controller, env, ctx): 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(
Object.assign(
{},
{
controller,
env,
},
ctx,
),
),
),
);
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment