Skip to content

Repository files navigation

Kuron

GitHub npm npm Bundle Size Bundle Size

A lightweight framework for Cloudflare Workers scheduled jobs with a Hono-inspired API.

Features

  • 🎯 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

Installation

npm install kuron

Kuron'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-types

Quick Start

import { Cron } from 'kuron';

const cron = new Cron()
  .schedule('0 15 * * *', async (c) => {
    console.log('☕️ Good afternoon! Time for your daily 3 PM cleanup routine ✨');
    // Your job logic here
  });

export default cron;

Or using a named function for better logging:

import { Cron } from 'kuron';

const cron = new Cron()
  .schedule('0 15 * * *', async function afternoonCleanup(c) {
    console.log('Running job:', c.name); // "afternoonCleanup"
    // Your job logic here
  });

export default cron;

API Reference

new Cron<Environment>()

Creates a new Cron instance.

Type Parameters:

  • Environment: Object with Bindings and Variables properties (similar to Hono)

.schedule(pattern, handler)

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');
});

.use(middleware)

Register middleware to run before job handlers.

Parameters:

  • middleware: Function with signature (c, next) => Promise<void>

Returns: this (chainable)

// Logging middleware
cron.use(async (c, next) => {
  console.log('Job started:', c.cron, c.name ? `(${c.name})` : '');
  const start = Date.now();
  await next();
  const duration = Date.now() - start;
  console.log(`Job completed in ${duration}ms`);
});

// Auth middleware
cron.use(async (c, next) => {
  if (!c.env.API_KEY) {
    throw new Error('API_KEY not configured');
  }
  await next();
});

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.

.onError(handler)

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.

Context Object (c)

The context object passed to handlers and middleware:

interface CronContext<E, P extends string = string> extends ScheduledController {
  // Environment bindings (secrets, KV namespaces, etc.)
  env: E['Bindings'];
  
  // Custom variables shared between middleware and handlers
  var: E['Variables'];
  
  // Cloudflare Workers execution context
  executionCtx: 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 variable
  get: <K extends keyof E['Variables']>(key: K) => E['Variables'][K];
  
  // Set a variable
  set: <K extends keyof E['Variables']>(key: K, value: E['Variables'][K]) => void;
}

Properties:

  • env: Access environment bindings (secrets, KV, D1, etc.)
  • var: Access/modify custom variables
  • executionCtx: Cloudflare Workers ExecutionContext for waitUntil() and passThroughOnException()
  • cron: The cron pattern string that triggered this job
  • name: Optional name of the handler function (captured from function.name)
  • scheduledTime: Unix timestamp (ms) when the job was scheduled (inherited from ScheduledController)
  • noRetry(): Call to prevent automatic retries on failure (inherited from ScheduledController)
  • get(key): Get a custom variable
  • set(key, value): Set a custom variable

Advanced Examples

Multiple Jobs

// Using anonymous functions
const cron = new Cron<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');
  });

Middleware Chain

const cron = new Cron<Environment>()
  // Timing middleware
  .use(async (c, next) => {
    const start = Date.now();
    await next();
    console.log(`Duration: ${Date.now() - start}ms`);
  })
  // Setup middleware
  .use(async (c, next) => {
    c.set('startTime', new Date().toISOString());
    await next();
  })
  // Cleanup middleware
  .use(async (c, next) => {
    await next();
    console.log('Cleanup completed');
  })
  .schedule('0 * * * *', async (c) => {
    const startTime = c.get('startTime');
    console.log('Job started at:', startTime);
  });

Sharing Data via Variables

interface MyEnvironment {
  Bindings: Env;
  Variables: {
    db: Database;
    requestId: string;
  };
}

const cron = new Cron<MyEnvironment>()
  .use(async (c, next) => {
    // Initialize DB connection in middleware
    const db = await initDatabase(c.env.DATABASE_URL);
    c.set('db', db);
    c.set('requestId', crypto.randomUUID());
    
    await next();
  })
  .schedule('0 15 * * *', async (c) => {
    // Access DB from middleware
    const db = c.get('db');
    const requestId = c.get('requestId');
    
    console.log('Request ID:', requestId);
    await db.query('...');
  });

Error Recovery

const cron = new Cron<Environment>()
  .use(async (c, next) => {
    try {
      await next();
    } catch (err) {
      console.error('Middleware caught error:', err);
      // Optionally rethrow or handle
      throw err;
    }
  })
  .schedule('0 * * * *', async (c) => {
    // Job logic that might fail
    await riskyOperation();
  })
  .onError(async (err, c) => {
    // Global error handling
    await reportToErrorService({
      error: err,
      cron: c.cron,
      name: c.name,
      timestamp: new Date().toISOString(),
    });
  });

Integration with Hono

import { OpenAPIHono } from '@hono/zod-openapi';
import { Cron } from 'kuron';

const app = new OpenAPIHono<Environment>()
  .get('/health', (c) => c.text('OK'));

const cron = new Cron<Environment>()
  .schedule('0 15 * * *', async (c) => {
    await syncData(c.env);
  });

export default {
  fetch: app.fetch,
  scheduled: cron.scheduled,
};

Cron Pattern Syntax

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 minutes
  • 0 0 * * SUN - Every Sunday at midnight
  • 0 0 1 * * - First day of every month at midnight
  • 30 2 * * MON-FRI - 2:30 AM UTC, Monday through Friday
  • 59 23 LW * * - 11:59 PM UTC on the last weekday of the month
  • 0 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 SUN or MON-FRI to avoid the ambiguity. Cloudflare also accepts Quartz extensions like L, LW, and 6L.

Patterns are validated when you call .schedule(), so a malformed pattern throws at startup rather than silently never firing.

TypeScript Tips

Type Inference

The Cron framework automatically infers types from your Environment:

interface MyEnvironment {
  Bindings: {
    DATABASE_URL: string;
  };
  Variables: {
    userId: string;
  };
}

const cron = new Cron<MyEnvironment>();

cron.schedule('0 * * * *', async (c) => {
  // TypeScript knows these types!
  c.env.DATABASE_URL; // string
  c.var.userId;       // string
  c.get('userId');    // string
  c.cron;             // "0 * * * *" (exact literal type!)
  c.name;             // string | undefined
  c.scheduledTime;    // number
});

Pattern Type Tracking

Similar to how Hono tracks route paths, the Cron framework tracks cron patterns in the type system:

// Single pattern
const dailyCron = new Cron<MyEnvironment>()
  .schedule('0 15 * * *', async (c) => {
    // c.cron has type: "0 15 * * *"
    console.log(c.cron);
  });

// Type: Cron<MyEnvironment, "0 15 * * *">

// Multiple patterns
const multiCron = new Cron<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 types
type ExtractPatterns<T> = T extends Cron<any, infer P> ? P : never;
type Patterns = ExtractPatterns<typeof multiCron>;
// 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

Best Practices

  1. Use Named Functions: Define handlers as named functions instead of anonymous arrow functions for better logging and debugging via c.name
  2. Use Middleware for Common Logic: Extract shared setup, logging, and cleanup into middleware
  3. Handle Errors Gracefully: Always implement .onError() for production workloads
  4. Keep Jobs Idempotent: Jobs should be safe to retry in case of failures
  5. Use ExecutionContext: Call c.executionCtx.waitUntil() for background tasks
  6. Log Extensively: Use middleware for consistent logging across all jobs, including c.name and c.cron
  7. Test Locally: Use Wrangler to test scheduled triggers locally

Comparison with Hono

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

License

MIT

About

A lightweight framework for Cloudflare Workers scheduled jobs with a Hono-inspired API.

Topics

Resources

Stars

22 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages