Skip to main content
Flora supports scheduling recurring tasks using cron expressions. All times are evaluated in UTC.

Basic Usage

cron('heartbeat', '* * * * *', (ctx) => {
  console.log(`Heartbeat at ${ctx.scheduledAt}`)
})

Cron Expression Format

Cron expressions use 5 fields:
minute hour day-of-month month day-of-week
minute
0-59
Minute of the hour
hour
0-23
Hour of the day (UTC)
day-of-month
1-31
Day of the month
month
1-12
Month of the year
day-of-week
0-7
Day of the week (0 and 7 are Sunday)

Special Characters

Matches any value. * * * * * means “every minute”.

Common Patterns

cron('tick', '* * * * *', (ctx) => {
  console.log('Tick')
})

Cron Context

Each cron handler receives a context object:
type CronContext = {
  name: string        // The job name you provided
  scheduledAt: string // ISO 8601 timestamp of scheduled time
}

Example

cron('logger', '0 * * * *', (ctx) => {
  console.log(`Job: ${ctx.name}`)
  console.log(`Scheduled for: ${ctx.scheduledAt}`)
  console.log(`Actual time: ${new Date().toISOString()}`)
})

Options

Skip If Running

Prevent overlapping executions:
cron('long-task', '*/5 * * * *', async (ctx) => {
  await someLongRunningTask()
}, { skipIfRunning: true })
If the previous execution is still running when the next one is due, the new execution is skipped.

Complete Example

// Daily cleanup at midnight UTC
cron('daily-cleanup', '0 0 * * *', async (ctx) => {
  const store = kv.store('cache')
  const result = await store.list({ prefix: 'temp:' })
  
  for (const key of result.keys) {
    await store.delete(key.name)
  }
  
  console.log(`Cleaned up ${result.keys.length} temporary entries`)
})

// Hourly stats report
cron('hourly-stats', '0 * * * *', async (ctx) => {
  const store = kv.store('stats')
  const count = await store.get('message_count')
  
  console.log(`Messages this hour: ${count || 0}`)
  
  // Reset counter
  await store.set('message_count', '0')
})

// Weekday reminder at 9 AM UTC
cron('weekday-reminder', '0 9 * * 1-5', async (ctx) => {
  // Send a reminder to a specific channel
  console.log('Time for daily standup!')
})

// Heavy task with skipIfRunning
cron('sync-data', '*/10 * * * *', async (ctx) => {
  // Long-running synchronization
  await syncExternalData()
}, { skipIfRunning: true })

Behavior Notes

Ephemeral State

Cron jobs are ephemeral — they exist only in memory:
  • Jobs are registered when your script loads
  • Jobs are cleared when you redeploy
  • Jobs are lost if the runtime restarts

Restart Behavior

After a restart:
  1. Scripts reload automatically. Your cron() calls run again, re-registering all jobs.
  2. Schedules resume, not catch up. Cron calculates the next run from “now” rather than executing missed runs.
  3. Redeployment clears old jobs. New script versions replace all previous cron jobs for that guild.

Crash Recovery

If the runtime crashes while a cron job is executing:
  • The is_running flag is lost
  • On restart, the job may run again if it’s due
  • Use skipIfRunning: true and design handlers to be idempotent
Idempotent handlers can run multiple times without side effects. For example, “set counter to 0” is idempotent; “increment counter” is not.

Limits

Max cron jobs per guild
32
Configurable via max_cron_jobs in runtime config.
Handler timeout
5 seconds
Configurable via cron_timeout_secs in runtime config.
If a handler exceeds the timeout, it will be terminated. Keep cron handlers fast and defer heavy work.

Type Definitions

export interface CronContext {
  name: string
  scheduledAt: string
}

export interface CronOptions {
  skipIfRunning?: boolean
}

export type CronHandler = (ctx: CronContext) => void | Promise<void>

declare function cron(
  name: string,
  cronExpr: string,
  handler: CronHandler,
  options?: CronOptions
): void

Examples by Use Case

Cleanup Old Data

cron('cleanup-old-logs', '0 2 * * *', async (ctx) => {
  const store = kv.store('logs')
  const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000  // 7 days ago
  
  const result = await store.list({ prefix: 'log:' })
  
  for (const key of result.keys) {
    const entry = await store.getWithMetadata(key.name)
    if (entry.metadata?.timestamp < cutoff) {
      await store.delete(key.name)
    }
  }
})

Periodic Status Updates

cron('status-update', '0 */6 * * *', async (ctx) => {
  const store = kv.store('metrics')
  const uptime = await store.get('uptime')
  
  console.log(`Status update: Uptime ${uptime}h`)
  // Could also send to a Discord channel
})

Rate Limit Reset

cron('reset-limits', '0 0 * * *', async (ctx) => {
  const store = kv.store('rate_limits')
  const result = await store.list({ prefix: 'limit:' })
  
  for (const key of result.keys) {
    await store.set(key.name, '0')
  }
  
  console.log('Rate limits reset')
})

Best Practices

1

Use descriptive names

Name jobs clearly: daily-backup, not job1.
2

Test cron expressions

Use a cron expression tester to verify timing before deploying.
3

Keep handlers fast

Handlers should complete within the 5-second timeout. Defer heavy work.
4

Make handlers idempotent

Design so running twice doesn’t cause issues (in case of crash recovery).
5

Log execution

Log when jobs run for debugging and monitoring.

Build docs developers (and LLMs) love