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 expressions use 5 fields:
minute hour day-of-month month day-of-week
Day of the week (0 and 7 are Sunday)
Special Characters
Asterisk (*)
Comma (,)
Dash (-)
Slash (/)
Matches any value. * * * * * means “every minute”.
List multiple values. 0,30 * * * * means “at minute 0 and 30”.
Range of values. 0 9-17 * * * means “every hour from 9 AM to 5 PM”.
Step values. */5 * * * * means “every 5 minutes”.
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:
- Scripts reload automatically. Your
cron() calls run again, re-registering all jobs.
- Schedules resume, not catch up. Cron calculates the next run from “now” rather than executing missed runs.
- 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
Configurable via max_cron_jobs in runtime config.
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
Use descriptive names
Name jobs clearly: daily-backup, not job1.
Test cron expressions
Use a cron expression tester to verify timing before deploying.
Keep handlers fast
Handlers should complete within the 5-second timeout. Defer heavy work.
Make handlers idempotent
Design so running twice doesn’t cause issues (in case of crash recovery).
Log execution
Log when jobs run for debugging and monitoring.