A lightweight framework for Cloudflare Workers scheduled jobs with a Hono-inspired API.
- 🎯 Hono-like API - Familiar and intuitive interface
- 🔗 Middleware Support - Chain middleware for logging, auth, etc.
- 🎭 Error Handling - Global error handler with context
- 🔐 Type Safety - Full TypeScript support with type inference
- 🎨 Context Variables - Share data across middleware and handlers
- ⚡ Zero Dependencies - Built on standard Cloudflare Workers APIs
npm install kuronKuron's types reference the Workers globals ScheduledController and ExecutionContext.
If your project does not already generate worker-configuration.d.ts via wrangler types,
install the types package alongside it:
npm install -D @cloudflare/workers-typesimport{Cron}from'kuron';constcron=newCron().schedule('0 15 * * *',async(c)=>{console.log('☕️ Good afternoon! Time for your daily 3 PM cleanup routine ✨');// Your job logic here});exportdefaultcron;Or using a named function for better logging:
import{Cron}from'kuron';constcron=newCron().schedule('0 15 * * *',asyncfunctionafternoonCleanup(c){console.log('Running job:',c.name);// "afternoonCleanup"// Your job logic here});exportdefaultcron;Creates a new Cron instance.
Type Parameters:
Environment: Object withBindingsandVariablesproperties (similar to Hono)
Register a scheduled job with a cron pattern.
Parameters:
pattern: Cron pattern string (e.g.,'0 15 * * *')handler: Async function to execute when the cron triggers
Returns:this (chainable)
cron.schedule('*/5 * * * *',async(c)=>{console.log('Runs every 5 minutes');});Register middleware to run before job handlers.
Parameters:
middleware: Function with signature(c, next) => Promise<void>
Returns:this (chainable)
// Logging middlewarecron.use(async(c,next)=>{console.log('Job started:',c.cron,c.name ? `(${c.name})` : '');conststart=Date.now();awaitnext();constduration=Date.now()-start;console.log(`Job completed in ${duration}ms`);});// Auth middlewarecron.use(async(c,next)=>{if(!c.env.API_KEY){thrownewError('API_KEY not configured');}awaitnext();});Call next() at most once per middleware. Skipping it short-circuits the chain and the
job handler never runs; calling it twice throws next() called multiple times rather than
silently running the handler again with part of the chain skipped.
Register a global error handler.
Parameters:
handler: Function with signature(error, context) => Promise<void>
Returns:this (chainable)
cron.onError((err,c)=>{console.error('Job failed:',{cron: c.cron,name: c.name,error: err.message,stack: err.stack,});// Optional: Send to error tracking service// await sendToSentry(err);});The handler is called once per failing job, with that job's own context. It also receives
the "no jobs registered for this pattern" misconfiguration, so a pattern that drifts out of
sync with wrangler.toml reaches your alerting instead of only console.warn.
Without .onError(), a failing job is logged and rethrown so the Workers runtime can retry
it — one failure rethrows as-is, several are combined into an AggregateError. Registering
.onError() marks errors as handled, which means the invocation reports success and the
runtime will not retry it. Rethrow from inside the handler if you want retries.
Jobs sharing a pattern run in sequence and are isolated from each other: one throwing does not cancel the rest, and each gets a fresh context, so variables set by middleware during one job are never visible to the next.
The context object passed to handlers and middleware:
interfaceCronContext<E,Pextendsstring=string>extendsScheduledController{// Environment bindings (secrets, KV namespaces, etc.)env: E['Bindings'];// Custom variables shared between middleware and handlersvar: E['Variables'];// Cloudflare Workers execution contextexecutionCtx: ExecutionContext;// Cron pattern for this job (e.g., "0 15 * * *")cron: P;// Name of the handler function (if provided as a named function)name?: string;// ScheduledController properties (inherited)// - scheduledTime: number// - noRetry(): void// Get a variableget: <KextendskeyofE['Variables']>(key: K)=>E['Variables'][K];// Set a variableset: <KextendskeyofE['Variables']>(key: K,value: E['Variables'][K])=>void;}Properties:
env: Access environment bindings (secrets, KV, D1, etc.)var: Access/modify custom variablesexecutionCtx: Cloudflare Workers ExecutionContext forwaitUntil()andpassThroughOnException()cron: The cron pattern string that triggered this jobname: Optional name of the handler function (captured fromfunction.name)scheduledTime: Unix timestamp (ms) when the job was scheduled (inherited fromScheduledController)noRetry(): Call to prevent automatic retries on failure (inherited fromScheduledController)get(key): Get a custom variableset(key, value): Set a custom variable
// Using anonymous functionsconstcron=newCron<Environment>().schedule('0 0 * * *',async(c)=>{console.log('Daily midnight job');}).schedule('0 12 * * *',async(c)=>{console.log('Daily noon job');}).schedule('0 0 * * SUN',async(c)=>{console.log('Weekly Sunday job');});constcron=newCron<Environment>()// Timing middleware.use(async(c,next)=>{conststart=Date.now();awaitnext();console.log(`Duration: ${Date.now()-start}ms`);})// Setup middleware.use(async(c,next)=>{c.set('startTime',newDate().toISOString());awaitnext();})// Cleanup middleware.use(async(c,next)=>{awaitnext();console.log('Cleanup completed');}).schedule('0 * * * *',async(c)=>{conststartTime=c.get('startTime');console.log('Job started at:',startTime);});interfaceMyEnvironment{Bindings: Env;Variables: {db: Database;requestId: string;};}constcron=newCron<MyEnvironment>().use(async(c,next)=>{// Initialize DB connection in middlewareconstdb=awaitinitDatabase(c.env.DATABASE_URL);c.set('db',db);c.set('requestId',crypto.randomUUID());awaitnext();}).schedule('0 15 * * *',async(c)=>{// Access DB from middlewareconstdb=c.get('db');constrequestId=c.get('requestId');console.log('Request ID:',requestId);awaitdb.query('...');});constcron=newCron<Environment>().use(async(c,next)=>{try{awaitnext();}catch(err){console.error('Middleware caught error:',err);// Optionally rethrow or handlethrowerr;}}).schedule('0 * * * *',async(c)=>{// Job logic that might failawaitriskyOperation();}).onError(async(err,c)=>{// Global error handlingawaitreportToErrorService({error: err,cron: c.cron,name: c.name,timestamp: newDate().toISOString(),});});import{OpenAPIHono}from'@hono/zod-openapi';import{Cron}from'kuron';constapp=newOpenAPIHono<Environment>().get('/health',(c)=>c.text('OK'));constcron=newCron<Environment>().schedule('0 15 * * *',async(c)=>{awaitsyncData(c.env);});exportdefault{fetch: app.fetch,scheduled: cron.scheduled,};Cloudflare Workers supports standard cron syntax:
┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of the month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of the week (1 - 7) (Sunday to Saturday)
│ │ │ │ │
* * * * *
Examples:
0 15 * * *- Daily at 3:00 PM UTC*/5 * * * *- Every 5 minutes0 0 * * SUN- Every Sunday at midnight0 0 1 * *- First day of every month at midnight30 2 * * MON-FRI- 2:30 AM UTC, Monday through Friday59 23 LW * *- 11:59 PM UTC on the last weekday of the month0 18 * * FRIL- 6:00 PM UTC on the last Friday of the month
Weekdays are numbered 1 (Sunday) through 7 (Saturday), which differs from the 0-6 convention used by Unix cron. Prefer three-letter abbreviations such as
SUNorMON-FRIto avoid the ambiguity. Cloudflare also accepts Quartz extensions likeL,LW, and6L.
Patterns are validated when you call .schedule(), so a malformed pattern throws at
startup rather than silently never firing.
The Cron framework automatically infers types from your Environment:
interfaceMyEnvironment{Bindings: {DATABASE_URL: string;};Variables: {userId: string;};}constcron=newCron<MyEnvironment>();cron.schedule('0 * * * *',async(c)=>{// TypeScript knows these types!c.env.DATABASE_URL;// stringc.var.userId;// stringc.get('userId');// stringc.cron;// "0 * * * *" (exact literal type!)c.name;// string | undefinedc.scheduledTime;// number});Similar to how Hono tracks route paths, the Cron framework tracks cron patterns in the type system:
// Single patternconstdailyCron=newCron<MyEnvironment>().schedule('0 15 * * *',async(c)=>{// c.cron has type: "0 15 * * *"console.log(c.cron);});// Type: Cron<MyEnvironment, "0 15 * * *">// Multiple patternsconstmultiCron=newCron<MyEnvironment>().schedule('0 * * * *',async(c)=>{// c.cron has type: "0 * * * *"}).schedule('0 0 * * *',async(c)=>{// c.cron has type: "0 0 * * *"});// Type: Cron<MyEnvironment, "0 * * * *" | "0 0 * * *">// Extract pattern typestypeExtractPatterns<T>=TextendsCron<any, infer P> ? P : never;typePatterns=ExtractPatterns<typeofmultiCron>;// Result: "0 * * * *" | "0 0 * * *"Benefits:
- 🎯 Type Safety: Catch typos in pattern comparisons at compile time
- 💡 IntelliSense: Better autocomplete and hover information
- 📚 Self-Documenting: See all patterns in type hints
- 🔄 Safe Refactoring: Rename patterns with confidence
- Use Named Functions: Define handlers as named functions instead of anonymous arrow functions for better logging and debugging via
c.name - Use Middleware for Common Logic: Extract shared setup, logging, and cleanup into middleware
- Handle Errors Gracefully: Always implement
.onError()for production workloads - Keep Jobs Idempotent: Jobs should be safe to retry in case of failures
- Use ExecutionContext: Call
c.executionCtx.waitUntil()for background tasks - Log Extensively: Use middleware for consistent logging across all jobs, including
c.nameandc.cron - Test Locally: Use Wrangler to test scheduled triggers locally
| Feature | Hono | Cron Framework |
|---|---|---|
| Entry point | app.fetch | cron.scheduled |
| Routing | URL patterns | Cron patterns |
| Context | Request-based | Schedule-based |
| Middleware | ✅ | ✅ |
| Variables | ✅ | ✅ |
| Error handling | ✅ | ✅ |
| Type safety | ✅ | ✅ |
MIT