Task Scheduling
@arkstack/scheduler lets you define scheduled tasks fluently in code instead of managing a crowd of cron entries on your server. You define the schedule once, and a single system cron entry drives everything.
Install
Full app templates include the scheduler and a src/routes/console.ts file. To add it manually:
pnpm add @arkstack/scheduler
ark publish --package @arkstack/scheduler # writes src/routes/console.tsDefining schedules
Define tasks in src/routes/console.ts using the Schedule facade:
import { Schedule } from '@arkstack/scheduler'
Schedule.command('cache:prune').hourly()
Schedule.call(async () => {
await pruneTempFiles()
})
.dailyAt('01:30')
.withoutOverlapping()
Schedule.job(new SendDigest()).weeklyOn(1, '08:00')
Schedule.exec('backup.sh').daily().onOneServer()Task types
| Method | Runs |
|---|---|
Schedule.command(name, args?) | An Arkstack CLI command (ark <name>) |
Schedule.call(fn) | A callback |
Schedule.job(job) | A queued job — dispatched via @arkstack/jobs |
Schedule.exec(cmd, args?) | A shell command |
Running the scheduler
The scheduler evaluates due tasks once a minute. In production, add one cron entry that calls schedule:run every minute:
* * * * * cd /path/to/app && npx ark schedule:run >> /dev/null 2>&1During development, run the foreground worker instead of a system cron:
ark schedule:work # evaluates the schedule every minute until stoppedInspect what's registered and when each task runs next:
ark schedule:listFrequencies
Frequency methods build the underlying cron expression:
Schedule.command('x').everyMinute()
Schedule.command('x').everyFiveMinutes() // */5 * * * *
Schedule.command('x').everyThirtyMinutes()
Schedule.command('x').hourly()
Schedule.command('x').hourlyAt(15) // 15 past every hour
Schedule.command('x').daily() // midnight
Schedule.command('x').dailyAt('13:00')
Schedule.command('x').twiceDaily(1, 13)
Schedule.command('x').weekly() // Sunday midnight
Schedule.command('x').weeklyOn(1, '8:00') // Monday 08:00
Schedule.command('x').monthly()
Schedule.command('x').monthlyOn(15, '17:00')
Schedule.command('x').quarterly()
Schedule.command('x').yearly()
Schedule.command('x').cron('*/10 9-17 * * 1-5') // raw cronConstrain the day of week, and layer time on top:
Schedule.command('x').weekdays().at('9:00')
Schedule.command('x').mondays()
Schedule.command('x').weekends().hourly()
Schedule.command('x').days([1, 4]) // Monday & ThursdaySet the timezone the expression is evaluated in:
Schedule.command('x').dailyAt('09:00').timezone('America/New_York')Constraints
Gate whether a due task actually runs:
Schedule.command('x')
.daily()
.when(async () => await featureEnabled('reports')) // run only when truthy
.skip(() => isHoliday()) // skip when truthy
.environments('production', 'staging') // limit by APP_ENV
.between('09:00', '17:00') // only within a daily windowUse unlessBetween('22:00', '06:00') to run outside a window (overnight ranges are handled).
Preventing overlaps & multi-server runs
Schedule.command('report:build').everyFiveMinutes().withoutOverlapping() // skip if the previous run is still going
Schedule.exec('backup.sh').daily().onOneServer() // run on only one server per due minuteBoth use @arkstack/cache for their locks, so they coordinate across processes and servers. Install and configure a shared cache store (e.g. redis or database) for onOneServer() to be effective; without a cache the scheduler falls back to a process-local lock.
Long-running commands can be pushed to the background so they don't delay other due tasks:
Schedule.exec('long-import.sh').daily().runInBackground()Hooks
Run callbacks around a task:
Schedule.command('report:build')
.daily()
.before(() => logger.info('building report'))
.onSuccess(() => notifyOk())
.onFailure((error) => notifyFailed(error))
.after(() => logger.info('done'))How it works
schedule:run loads src/routes/console.ts, finds the events whose cron expression is due in the current minute, checks their constraints, acquires any locks, and runs them — resolving cron with croner. Because everything is evaluated per minute, a single * * * * * cron entry is all your server needs.
