Skip to main content
The @ooneex/cron component runs recurring background tasks. You extend the abstract Cron class, declare a schedule with a readable string like "every 1 hours", and implement a handler() that runs on that schedule. Behind the scenes the schedule is converted to a crontab expression and driven by Bun’s native Bun.cron, so there is no external scheduler to operate.

Why this component

  • Human-readable schedules. Express timing as "every 5 minutes" or "in 30 seconds" instead of raw crontab syntax — conversion is automatic.
  • One class per job. Each job is a class that implements three methods: getTime(), getTimeZone(), and handler().
  • Timezone aware. Return an IANA timezone (e.g. "Europe/Paris") or null to use the server’s local time.
  • Lifecycle control. Start, stop, and inspect a job with start(), stop(), and isActive().
  • Container-managed. Register a job with @decorator.cron() and resolve it from the container.

How it works

A cron job is a class that extends Cron and implements its three abstract methods. When you call start(), the schedule from getTime() is converted to a crontab expression and registered with Bun.cron, which invokes your handler() on every tick.
MethodPurpose
getTime()Return the schedule as a CronTimeType string, e.g. "every 1 hours".
getTimeZone()Return an IANA timezone string, or null for the server’s local timezone.
handler()The async work to run on each scheduled tick.
start()Convert the schedule and register the job. No-op if already active. Throws CronException on failure.
stop()Stop the job and release the underlying timer.
isActive()Whether the job is currently scheduled.
The schedule string has the shape "<prefix> <number> <unit>":
PartValues
Prefixevery (recurring) or in (one-time, after the delay)
NumberAny positive integer
Unitseconds, minutes, hours, days, months, years
So "every 5 minutes" runs repeatedly every five minutes, while "in 30 seconds" schedules a single run thirty seconds from when the job starts. An invalid format or a non-positive number throws CronException.

Decorator and usage

@decorator.cron()

Registers a cron class with the container. It accepts an optional scope (defaults to singleton). Decorate any class that extends Cron.
import type { TimeZoneType } from "@ooneex/country";
import { Cron, decorator, type CronTimeType } from "@ooneex/cron";

@decorator.cron()
export class CleanupCron extends Cron {
  public getTime(): CronTimeType {
    return "every 1 hours";
  }

  public getTimeZone(): TimeZoneType | null {
    return null; // server's local timezone
  }

  public async handler(): Promise<void> {
    await this.removeExpiredRecords();
  }

  private async removeExpiredRecords(): Promise<void> {
    // cleanup logic
  }
}
Resolve and control the job from the container:
import { container } from "@ooneex/container";

const cleanup = container.get(CleanupCron);

await cleanup.start();
cleanup.isActive(); // true

await cleanup.stop();
cleanup.isActive(); // false
Run a job in a specific timezone by returning an IANA name from getTimeZone():
import type { TimeZoneType } from "@ooneex/country";
import { Cron, decorator, type CronTimeType } from "@ooneex/cron";

@decorator.cron()
export class DailyReportCron extends Cron {
  public getTime(): CronTimeType {
    return "every 1 days";
  }

  public getTimeZone(): TimeZoneType | null {
    return "Europe/Paris";
  }

  public async handler(): Promise<void> {
    await this.generateDailyReport();
  }

  private async generateDailyReport(): Promise<void> {
    // report logic
  }
}
Use the in prefix for a one-time delayed run:
public getTime(): CronTimeType {
  return "in 30 seconds"; // runs once, 30 seconds after start()
}

Exceptions

The component throws CronException when a schedule is malformed or a job fails to start. It carries a machine-readable key, a human-readable message, and a data object.
KeyWhen
INVALID_FORMATThe schedule string is not in "<prefix> <number> <unit>" form.
INVALID_VALUEThe number in the schedule is not a positive integer.
START_FAILEDThe underlying scheduler rejected the converted crontab expression when starting.
import { CronException } from "@ooneex/cron";

try {
  await job.start();
} catch (error) {
  if (error instanceof CronException) {
    logger.error(`Cron error [${error.key}]: ${error.message}`, error.data);
  } else {
    throw error;
  }
}

Best practices

  • Catch errors inside handler(). A throw in the handler should not take down the scheduler — wrap risky work and log failures.
  • Keep handlers idempotent. A run may overlap or repeat; make the work safe to execute more than once.
  • Match the interval to the work. Avoid scheduling a job more often than it can finish; a long task on "every 1 minutes" can pile up.
  • Pin a timezone for time-sensitive jobs. Return an explicit IANA zone for reports and billing so behavior is stable across servers; use null only when local time is fine.
  • Resolve shared dependencies from the container. Inject caches, databases, and loggers rather than constructing them inside the handler.
  • Use in for delayed one-shots, every for recurring work. Pick the prefix that matches the intent instead of approximating with a long interval.
  • Stop jobs you no longer need. Call stop() to release the timer; check isActive() before assuming a job is running.

CLI command

Scaffold a cron class and its test file with the generator. It writes the class under modules/<module>/src/crons/<Name>Cron.ts, registers it in the module’s cronJobs array, and installs @ooneex/cron if it is missing.
# Interactive: prompts for the name
ooneex cron:create

# Provide the name
ooneex cron:create --name=Cleanup

# Target a module and overwrite
ooneex cron:create --name=Cleanup --module=auth --override
OptionDescriptionDefault
--nameCron class name. The Cron suffix is appended automatically.Prompted if omitted
--moduleTarget module the class is generated into.shared
--overrideOverwrite an existing class without prompting.false
The generated class is a ready-to-fill stub with the schedule, timezone, and handler in place:
import type { TimeZoneType } from "@ooneex/country";
import type { CronTimeType } from "@ooneex/cron";
import { Cron, decorator } from "@ooneex/cron";

@decorator.cron()
export class CleanupCron extends Cron {
  public getTime(): CronTimeType {
    // Examples: "every 5 minutes", "every 1 hours", "every 30 seconds"
    return "every 1 hours";
  }

  public getTimeZone(): TimeZoneType | null {
    // Return null to use server timezone, or specify a timezone like "Europe/Paris"
    return null;
  }

  public async handler(): Promise<void> {
    // Implement your cron handler logic here
    // console.log("CleanupCron handler executed");
  }
}
See cron:create for the full command reference.

Use with Claude and Codex

The generator ships a matching cron:create skill. It runs the scaffold and then guides your AI agent through completing the job — setting the schedule in getTime(), choosing a timezone, and implementing handler() with real logic. Initialize the skills once for your agent:
ooneex claude:init
Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the implementation:
Prompt
Create a cron job that runs every night at midnight to clean up expired sessions.
For example, the prompt above maps to cron:create --name=CleanExpiredSessions, then sets the schedule and implements the handler() to remove expired sessions.