OqronKitOqronKit

Scheduler

Cron, RRule, one-shot, and repeating interval schedules

Scheduler

OqronKit provides two scheduling APIs: cron() for fixed infrastructure jobs, and schedule() for dynamic, data-driven scheduling.

Cron vs Schedule

cron()schedule()
TriggerTime-driven (expression/every)Data-driven (API-triggered)
PayloadNoneTyped generic payload
Dynamic dispatchNo.trigger() / .schedule()
ConditionsNocondition: async (ctx) => boolean
Best forSweeps, cleanup, metricsDrip campaigns, delayed actions

Cron

Expression-based

triggers/crons.ts
import { cron, type ICronContext } from 'oqronkit'

export const dailyReport = cron({
  name: 'daily-analytics-report',
  expression: '0 8 * * *',       // Every day at 8 AM
  timezone: 'Asia/Kolkata',

  priority: 1,                    // Lower = fires first among simultaneous crons
  version: 2,                     // Bump to trigger config migration
  missedFire: 'run-once',         // Recover missed fires
  overlap: 'skip',                // Skip if previous run is still active
  guaranteedWorker: true,         // Heartbeat crash-safety
  timeout: 120_000,

  tags: ['analytics', 'reporting'],
  keepHistory: 30,
  keepFailedHistory: true,

  hooks: {
    beforeRun: async (ctx) => {
      ctx.log.info('📊 Report generation starting...')
    },
    afterRun: async (ctx, result) => {
      ctx.log.info('✅ Report completed', { duration: `${ctx.duration}ms` })
    },
    onError: async (ctx, error) => {
      ctx.log.error('🔥 Report FAILED', { error: error.message })
    },
    onMissedFire: async (ctx, missedAt) => {
      ctx.log.warn('⏰ Missed fire recovered', { missedAt: missedAt.toISOString() })
    },
  },

  handler: async (ctx: ICronContext) => {
    ctx.progress(10, 'Querying raw events')
    ctx.progress(50, 'Aggregating metrics')
    ctx.progress(100, 'Done')
    return { rowsProcessed: 154_200, tenants: 42 }
  },
})

Interval-based (every)

export const healthCheck = cron({
  name: 'health-check-ping',
  every: { seconds: 10 },
  jitterMs: 3_000,      // Prevent thundering herd across cluster
  priority: 100,         // Low priority — never block critical crons
  missedFire: 'skip',
  overlap: 'run',
  keepHistory: false,

  handler: async (ctx: ICronContext) => {
    ctx.log.debug('💓 Health check', {
      memoryMb: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
    })
    return { status: 'ok' }
  },
})

Schedule

One-off (runAt)

triggers/scheduler.ts
import { schedule, type IScheduleContext } from 'oqronkit'

export const dataMigration = schedule({
  name: 'data-migration-v2',
  runAt: new Date('2026-12-01T00:00:00Z'),
  guaranteedWorker: true,
  priority: 0,

  handler: async (ctx: IScheduleContext) => {
    ctx.progress(50, 'Migrating rows')
    return { rowsMigrated: 54_000 }
  },
})

Repeating interval (every)

export const metricsAgg = schedule({
  name: 'metrics-aggregation',
  every: { minutes: 5 },
  jitterMs: 15_000,
  priority: 20,
  overlap: 'skip',

  handler: async (ctx) => {
    ctx.log('info', '📈 Aggregating metrics')
    return { eventsProcessed: 142_000 }
  },
})

Recurring calendar (recurring)

export const quarterlyReview = schedule({
  name: 'quarterly-financial-review',
  recurring: {
    frequency: 'monthly',
    dayOfMonth: 1,
    at: { hour: 9, minute: 0 },
    months: [1, 4, 7, 10],
  },
  timezone: 'Europe/London',

  condition: async (ctx) => {
    const day = new Date().getDay()
    return day > 0 && day < 6 // Skip weekends
  },

  handler: async (ctx) => {
    return { mrr: 284_500, churnRate: 2.1 }
  },
})

iCalendar (rrule)

export const payrollRun = schedule({
  name: 'payroll-processing',
  rrule: 'FREQ=MONTHLY;BYDAY=-1FR',  // Last Friday of every month
  guaranteedWorker: true,
  priority: 0,
  maxConcurrent: 1,

  handler: async (ctx) => {
    ctx.progress(40, 'Calculating taxes')
    ctx.progress(100, 'Payroll complete')
    return { employeesPaid: 324 }
  },
})

Dynamic Templates (.trigger() / .schedule())

Define a schedule with no timer — it only fires when you call .trigger() or .schedule() with a payload:

export const onboardingEmail = schedule<{
  userId: string
  template: string
  email: string
}>({
  name: 'onboarding-email',
  // No every/runAt/recurring — this is a TEMPLATE

  handler: async (ctx) => {
    const { userId, template, email } = ctx.payload
    ctx.log('info', `Sending ${template} to ${email}`)
    return { sent: true }
  },
})

// Usage from your API:
await onboardingEmail.trigger({
  payload: { userId: 'u_123', template: 'welcome', email: 'user@ex.com' },
})

await onboardingEmail.schedule({
  nameSuffix: 'u_123-tips',
  runAfter: { days: 3 },
  payload: { userId: 'u_123', template: 'day3-tips', email: 'user@ex.com' },
})

Configuration Reference

OptionTypeDescription
namestringUnique schedule identifier
expressionstringUNIX cron expression (cron only)
everyEveryConfigInterval: weeks, days, hours, minutes, seconds
runAtDateOne-shot execution at a specific time
recurringScheduleRecurringSemantic calendar builder
rrulestringRFC 5545 recurrence rule
timezonestringIANA timezone
missedFire'skip' | 'run-once' | 'run-all'Behavior for missed fires
overlap'skip' | 'run'Overlap handling
jitterMsnumberRandom jitter to prevent thundering herd
prioritynumberLower = fires first
versionnumberBump to trigger config migration
rateLimiter{ check() }Optional rate limit gate
condition(ctx) => booleanConditional execution (schedule only)
maxConcurrentnumberMax parallel runs

Missed Fire Policies

When a scheduled fire is missed (e.g. server was down during the scheduled time), the missedFire option controls recovery:

PolicyBehavior
'skip'Ignore missed fires entirely
'run-once'Fire once for the most recent missed occurrence
'run-all'Fire once for each missed occurrence (capped by maxMissedRuns, default: 100)

v0.0.2: missedFire: "run-all" now correctly enumerates all missed occurrences using MissedFireHandler. In v0.0.1, it only fired once regardless of how many ticks were missed.

Next Steps

On this page