Cron
Schedule recurring tasks with cron expressions. A curated set of named Patterns covers the most common intervals without memorizing cron syntax.
Introduction
@tekir/cron wraps the croner library (install with bun add croner) and exposes a named-job API through Cron.
import { service } from '@tekir/core'
import type { Cron } from '@tekir/cron'
export const cron = service<Cron>('cron')Register CronProvider in your kernel:
import type { TekirApp } from '@tekir/core'
import { CronProvider } from '@tekir/cron'
export default function({ app }: TekirApp) {
app.registerAll([CronProvider])
}Jobs are referenced by a string name, which makes it straightforward to pause, resume, or remove them later. Cron patterns use six fields (seconds included), e.g. 0 0 8 * * * means "08:00:00 every day".
Adding Jobs
cron.add(name, pattern, callback) is async because it dynamically imports the croner package on first use. The callback is started immediately after registration. Registering the same name twice throws an error, call cron.remove(name) first if you need to replace a job.
import type { TekirApp } from '@tekir/core'
import { cron } from '#services'
import { Patterns } from '@tekir/cron'
export default async function({ app }: TekirApp) {
// cron.add(name, pattern, callback): registers and starts immediately.
await cron.add('send-daily-digest', Patterns.daily, async () => {
await sendDigestEmails()
})
await cron.add('cleanup-temp-files', Patterns.hourly, async () => {
await deleteTempFiles()
})
// Use a raw cron expression (6-field: seconds included)
await cron.add('heartbeat', '*/30 * * * * *', async () => {
await pingHealthCheck()
})
}Errors thrown inside the callback are caught and logged to stderr; they do not crash the process or unregister the job.
Patterns
The Patterns object exports pre-built cron expressions for the most common schedules. All expressions use the six-field format (second, minute, hour, day, month, weekday).
Pattern Reference
import { Patterns } from '@tekir/cron'
// Every second
Patterns.everySecond // '* * * * * *'
// Every minute (at second :00)
Patterns.everyMinute // '0 * * * * *'
// Every N minutes
Patterns.everyFiveMinutes // '0 */5 * * * *'
Patterns.everyTenMinutes // '0 */10 * * * *'
Patterns.everyFifteenMinutes // '0 */15 * * * *'
Patterns.everyThirtyMinutes // '0 */30 * * * *'
// Hourly (at minute :00, second :00)
Patterns.hourly // '0 0 * * * *'
// Daily at midnight
Patterns.daily // '0 0 0 * * *'
// Weekly: every Sunday at midnight
Patterns.weekly // '0 0 0 * * 0'
// First day of every month at midnight
Patterns.monthly // '0 0 0 1 * *'
// First day of January every year at midnight
Patterns.yearly // '0 0 0 1 1 *'dailyAt / weeklyOn
Patterns.dailyAt(hour, minute?) and Patterns.weeklyOn(day, hour?, minute?) are factory functions that return a cron expression string. Days are numbered 0 (Sunday) through 6 (Saturday).
import { Patterns } from '@tekir/cron'
// Every day at 08:30
const pattern = Patterns.dailyAt(8, 30)
// → '0 30 8 * * *'
await cron.add('morning-report', Patterns.dailyAt(8, 30), async () => {
await generateMorningReport()
})
// Every Friday at 17:00 (day 5 = Friday, 0 = Sunday)
const fridayClose = Patterns.weeklyOn(5, 17, 0)
// → '0 0 17 * * 5'
await cron.add('end-of-week-summary', Patterns.weeklyOn(5, 17), async () => {
await sendWeekSummary()
})Pausing & Resuming
cron.stop(name) pauses a job without removing it from the registry, the next scheduled tick is simply skipped. cron.start(name) resumes it. Both are no-ops if the job is already in the desired state.
// Pause a running job, the schedule is remembered, the callback will not fire.
cron.stop('send-daily-digest')
// Resume a paused job.
cron.start('send-daily-digest')Removing Jobs
cron.remove(name) stops the underlying croner handle and deletes the job from the registry. Throws if the name is not registered.
// Stop and permanently unregister a job.
// Throws if the name is not found.
cron.remove('cleanup-temp-files')
// Safe remove: check first if you are not sure.
if (cron.isRunning('cleanup-temp-files') || cron.list().some(j => j.name === 'cleanup-temp-files')) {
cron.remove('cleanup-temp-files')
}Listing Jobs
cron.list() returns a snapshot of every registered job as a JobInfo[]. Each entry has name, pattern, and running (the inverse of paused).
// Returns a snapshot of all registered jobs.
const jobs = cron.list()
for (const job of jobs) {
console.log(job.name) // 'send-daily-digest'
console.log(job.pattern) // '0 0 0 * * *'
console.log(job.running) // true or false
}
// Example output:
// [
// { name: 'send-daily-digest', pattern: '0 0 0 * * *', running: true },
// { name: 'heartbeat', pattern: '*/30 * * * * *', running: true },
// ]isRunning
cron.isRunning(name) returns true if the named job exists and is not paused. Returns false for unregistered names without throwing.
// Returns true if the job is registered and not paused.
if (cron.isRunning('heartbeat')) {
console.log('Heartbeat is active')
}
// Returns false for unregistered names (no throw).
console.log(cron.isRunning('nonexistent')) // falseBulk Operations
cron.stopAll() and cron.startAll() pause or resume every registered job at once. Useful during deployments or graceful-shutdown handlers.
import { cron } from '#services'
// Pause every registered job at once.
cron.stopAll()
// Resume every registered job at once.
cron.startAll()
// Useful on graceful shutdown:
process.on('SIGTERM', () => {
cron.stopAll()
process.exit(0)
})Decorators
@tekir/cron-decorators provides class and method decorators for a declarative approach. Decorate a class with @CronJob(), then annotate methods with @Schedule(pattern) or @Every(duration).
import { CronJob, Schedule, Every } from '@tekir/cron-decorators'
@CronJob()
class MaintenanceJobs {
// Run at a specific cron expression
@Schedule('0 0 3 * * *')
async cleanupTempFiles() {
await deleteTempFiles()
}
// Run every N duration
@Every('5m')
async syncMetrics() {
await pushMetrics()
}
// Named pattern shortcut
@Schedule('daily')
async sendDigest() {
await sendDigestEmails()
}
}Folder registration
For projects with many jobs in core/jobs/, call cron.registerDir(path) instead of importing every job explicitly. The manager loads each file and routes it to the right registration path: decorator classes go through cron.register(Class), functional registrars (export default (cron) => { ... }) are invoked with the manager, and classes with a register(cron) method are constructed and called.
// core/jobs/cleanup.ts
import { CronJob, Schedule } from '@tekir/cron-decorators'
@CronJob()
export default class CleanupJob {
@Schedule('0 0 3 * * *')
async run() { await deleteTempFiles() }
}
// start/boot.ts (or wherever you have access to the cron service)
import { service } from '@tekir/core'
import type { Cron } from '@tekir/cron'
const cron = service<Cron>('cron')
// Loads every file in core/jobs/ and registers each one. Auto-detects
// decorator classes, functional registrars, and classes with a
// register(cron) method. Replaces the long list of explicit imports.
await cron.registerDir('core/jobs')Files whose default export does not match any pattern are skipped with a console warning so misconfigured exports surface during boot. For bun build --compile, install oxc-parser as a dev dependency (bun add -d oxc-parser) and the compile pipeline replaces every literal-string registerDir call with explicit static imports before Bun bundles, so the binary contains every job. See the Inline API page for the autoload compile section.