Skip to content

Examples

Runnable code examples for the most common Kronos use cases.


Defining a Job

A Job is a Kotlin object (or class) that implements the Job interface. Override only the callbacks you need.

object SendReport : Job {
    override val name = "send-report"
    override val retries = 2

    override suspend fun execute(cycleNumber: Int, params: Map<String, Any>): Boolean {
        val reportId = params["reportId"] as String
        sendReport(reportId)
        return true          // return false to trigger a retry
    }

    override fun onSuccess(cycleNumber: Int, params: Map<String, Any>) {
        println("Report sent on cycle $cycleNumber")
    }

    override fun onRetryFail(retryCount: Int, cycleNumber: Int, params: Map<String, Any>, exception: Exception?) {
        println("Attempt $retryCount failed: ${exception?.message}")
    }

    override fun onFail(cycleNumber: Int, params: Map<String, Any>, exception: Exception?) {
        alertOncall("send-report failed after all retries: ${exception?.message}")
    }

    override fun onDrop(jobId: String, lastJob: Boolean) {
        if (lastJob) println("No more send-report jobs queued")
    }
}

Scheduling a Job

Kronos.register(SendReport)

// One-time, runs on the next tick
Kronos.schedule(
    jobName = SendReport.name,
    params = mapOf("reportId" to "rpt-001")
)

// One-time, delayed by 1 hour
Kronos.schedule(
    jobName = SendReport.name,
    delay = 1.hours,
    params = mapOf("reportId" to "rpt-002")
)

// One-time, at an absolute UTC timestamp
Kronos.schedule(
    jobName = SendReport.name,
    startTime = ZonedDateTime.of(2024, 12, 1, 9, 0, 0, 0, ZoneOffset.UTC).toEpochSecond() * 1000,
    params = mapOf("reportId" to "rpt-003")
)

Dropping Jobs

val jobId = Kronos.schedule(jobName = SendReport.name, params = emptyMap())!!

// Cancel one specific job
Kronos.dropJobId(jobId)

// Cancel all jobs with a given name
Kronos.dropJob(SendReport.name)

// Cancel every scheduled job
Kronos.dropAll()

Retrying Failed Jobs

Set retries on the Job object. Each failure calls onRetryFail; onFail fires only after all retries are exhausted.

object FlakyScraper : Job {
    override val name = "scraper"
    override val retries = 3   // 1 initial attempt + 3 retries = 4 total attempts

    override suspend fun execute(cycleNumber: Int, params: Map<String, Any>): Boolean {
        return try {
            scrapeUrl(params["url"] as String)
            true
        } catch (e: IOException) {
            false   // triggers a retry
        }
    }

    override fun onRetryFail(retryCount: Int, cycleNumber: Int, params: Map<String, Any>, exception: Exception?) {
        println("Retry $retryCount/3 failed")
    }

    override fun onFail(cycleNumber: Int, params: Map<String, Any>, exception: Exception?) {
        println("All retries exhausted for ${params["url"]}")
    }
}

You can also override retries per-schedule call:

Kronos.schedule(
    jobName = FlakyScraper.name,
    retries = 5,               // overrides Job.retries for this instance only
    params = mapOf("url" to "https://example.com")
)

Scheduling Recursively Inside execute()

A job can schedule new jobs from within its own execute — useful for fan-out or conditional follow-up work.

object ProcessBatch : Job {
    override val name = "process-batch"

    override suspend fun execute(cycleNumber: Int, params: Map<String, Any>): Boolean {
        val items = fetchNextBatch()

        // Schedule a follow-up for each item
        items.forEach { item ->
            Kronos.schedule(
                jobName = SendNotification.name,
                delay = 5.minutes,
                params = mapOf("itemId" to item.id)
            )
        }

        // Reschedule self if more batches remain
        if (hasMoreBatches()) {
            Kronos.schedule(
                jobName = name,
                delay = 1.minutes,
                params = params
            )
        }

        return true
    }
}

Tracking the Next Periodic Job ID

periodicJobLoaded is called as soon as the next occurrence is inserted into the database — before the current cycle finishes executing. Use it to track the live job ID.

object DailySync : Job {
    override val name = "daily-sync"

    @Volatile var currentJobId: String? = null

    override fun periodicJobLoaded(originJobId: String, nextJobId: String) {
        currentJobId = nextJobId
        println("Next daily-sync scheduled as $nextJobId")
    }
}

// To cancel the next pending run at any time:
DailySync.currentJobId?.let { Kronos.dropJobId(it) }

Challenge Job Execution

challengeRun is a runtime gate called before each execution. Return true to skip the cycle, false to allow it. Use this for scheduling patterns that Periodic alone cannot express.

Skip on public holidays

object Payroll : Job {
    override val name = "payroll"

    override fun challengeRun(cycleNumber: Int, params: Map<String, Any>): Boolean {
        return isPublicHoliday(LocalDate.now(ZoneOffset.UTC))
    }
}

Run only on Tuesdays (schedule daily, gate on day)

object TuesdayReport : Job {
    override val name = "tuesday-report"

    override fun challengeRun(cycleNumber: Int, params: Map<String, Any>): Boolean {
        return LocalDate.now(ZoneOffset.UTC).dayOfWeek != DayOfWeek.TUESDAY
    }
}

// Schedule daily — challengeRun filters to Tuesdays only
Kronos.schedulePeriodic(
    jobName = TuesdayReport.name,
    periodic = Periodic.everyDay(hour = 9, minute = 0),
    params = emptyMap()
)

Bi-weekly (run every other cycle)

object BiWeeklyDigest : Job {
    override val name = "bi-weekly-digest"

    override fun challengeRun(cycleNumber: Int, params: Map<String, Any>): Boolean {
        return cycleNumber % 2 != 0    // skip odd cycles, run on even
    }
}

Kronos.schedulePeriodic(
    jobName = BiWeeklyDigest.name,
    periodic = Periodic.everyWeek(dayOfWeek = 1, hour = 8, minute = 0),
    params = emptyMap()
)

Second Tuesday of the month

object MonthlyTeamUpdate : Job {
    override val name = "monthly-team-update"

    override fun challengeRun(cycleNumber: Int, params: Map<String, Any>): Boolean {
        val today = LocalDate.now(ZoneOffset.UTC)
        val isTuesday = today.dayOfWeek == DayOfWeek.TUESDAY
        val isSecondWeek = today.dayOfMonth in 8..14
        return !(isTuesday && isSecondWeek)
    }
}

// Schedule weekly on Tuesday — challengeRun narrows it to the 2nd occurrence
Kronos.schedulePeriodic(
    jobName = MonthlyTeamUpdate.name,
    periodic = Periodic.everyWeek(dayOfWeek = 2, hour = 10, minute = 0),
    params = emptyMap()
)

Periodic Schedule Examples

Every minute

Kronos.schedulePeriodic(
    jobName = "health-check",
    periodic = Periodic.everyMinute(),
    params = emptyMap()
)

Every hour at a fixed minute

Kronos.schedulePeriodic(
    jobName = "cache-refresh",
    periodic = Periodic.everyHour(minute = 30),   // runs at :30 past every hour
    params = emptyMap()
)

Every 32 minutes (interval-based, not calendar-aligned)

Use schedule with an interval for non-standard intervals that don't map to a clock pattern:

Kronos.schedule(
    jobName = "metrics-flush",
    interval = 32.minutes,
    params = emptyMap()
)

Every day at a specific time

Kronos.schedulePeriodic(
    jobName = "daily-report",
    periodic = Periodic.everyDay(hour = 9, minute = 0),   // 09:00 UTC
    params = emptyMap()
)

Every 2 days

Kronos.schedule(
    jobName = "alternate-day-sync",
    interval = 48.hours,
    params = emptyMap()
)

Every week on a specific day

Kronos.schedulePeriodic(
    jobName = "weekly-digest",
    periodic = Periodic.everyWeek(dayOfWeek = 5, hour = 17, minute = 0),  // Friday 17:00 UTC
    params = emptyMap()
)

Every month on a specific day and time

Kronos.schedulePeriodic(
    jobName = "monthly-invoice",
    periodic = Periodic.everyMonth(dayOfMonth = 1, hour = 8, minute = 0),  // 1st of each month
    params = mapOf("type" to "invoice")
)

Every year on a specific date

Kronos.schedulePeriodic(
    jobName = "annual-audit",
    periodic = Periodic.everyYear(month = 1, dayOfMonth = 1, hour = 0, minute = 0),  // Jan 1 midnight UTC
    params = emptyMap()
)

Comments