Skip to main content

stacks-scheduler

Use when scheduling tasks in a Stacks application — defining scheduled tasks, cron-like scheduling, or task automation. Covers @stacksjs/scheduler, @stacksjs/cron, and app/Scheduler.ts.

Source facts

Repository
stacksjs/system-cleaner
Last source activity
August 29, 2026 at 00:22
Detected SKILL.md language
English
Stars
2
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
stacks-scheduler
description
Use when scheduling tasks in a Stacks application — defining scheduled tasks, cron-like scheduling, or task automation. Covers @stacksjs/scheduler, @stacksjs/cron, and app/Scheduler.ts.
license
MIT
compatibility
Bun >= 1.3.0, TypeScript
allowed-tools
Read Edit Write Bash Grep Glob
# Stacks Scheduler The `@stacksjs/scheduler` package provides fluent, chainable task scheduling for Stacks applications. It wraps `@stacksjs/cron` for cron expression parsing and supports scheduling jobs, actions, and shell commands with timezone awareness, overlap prevention, and background execution. ## Key Paths - Scheduler package: `storage/framework/core/scheduler/src/` - Cron package: `storage/framework/core/cron/src/` - Application scheduler: `app/Scheduler.ts` - CLI command: `storage/framework/core/buddy/src/commands/schedule.ts` - Run action: `storage/framework/core/actions/src/schedule/run.ts` - Queue-based scheduler: `storage/framework/core/queue/src/scheduler.ts` - Job types / `Every` enum: `storage/framework/core/types/src/cron-jobs.ts` - Lock files: `storage/framework/locks/` (created at runtime) ## Source Files ``` scheduler/src/ ├── index.ts # Re-exports everything from run, schedule, types ├── schedule.ts # Schedule class, Queue alias, sendAt(), timeout() ├── run.ts # runScheduler() — loads Jobs/*.ts + app/Scheduler.ts └── types.ts # Timezone union, ScheduledJob, UntimedSchedule, TimedSchedule, BaseSchedule cron/src/ ├── index.ts # parse(), register(), remove() — delegates to Bun.cron or parseCron ├── parser.ts # parseCron() — native 5-field cron parser with POSIX OR logic ├── types.ts # CatchCallbackFn, ProtectCallbackFn, IntRange └── bun-cron.d.ts # Bun.cron type declarations queue/src/ └── scheduler.ts # startScheduler(), stopScheduler(), getSchedulerStatus() — queue-level scheduler ``` ## Schedule Class (schedule.ts) The `Schedule` class is the core scheduling API. The lowercase `schedule` export is an alias for the class itself (used as a static-method namespace). ```typescript import { schedule } from '@stacksjs/scheduler' // Static factory methods — each returns UntimedSchedule schedule.job(name: JobName): UntimedSchedule // Runs a job by name via runJob() schedule.action(name: string): UntimedSchedule // Runs an action by name via runAction() schedule.command(cmd: string): UntimedSchedule // Runs a shell command via runCommand() // Graceful shutdown — stops all tracked jobs schedule.gracefulShutdown(): Promise<void> ``` ### Constructor ```typescript new Schedule(task: () => void) ``` The constructor accepts a task function and auto-starts the schedule via `setTimeout(() => this.start(), 0)` after all chained methods have been called in the current tick. ### Timing Methods (returns TimedSchedule) Each sets an internal cron pattern. Once a timing method is called, the schedule is "timed" and only configuration methods remain available. | Method | Cron Pattern | Notes | |------------------------|--------------------|-----------------------------| | `everySecond()` | `@every_second` | Uses `setInterval(1000)`, not cron | | `everyMinute()` | `* * * * *` | | | `everyTwoMinutes()` | `*/2 * * * *` | | | `everyFiveMinutes()` | `*/5 * * * *` | | | `everyTenMinutes()` | `*/10 * * * *` | | | `everyThirtyMinutes()` | `*/30 * * * *` | | | `everyHour()` / `hourly()` | `0 * * * *` | | | `everyDay()` / `daily()` | `0 0 * * *` | | | `weekly()` | `0 0 * * 0` | Sunday at midnight | | `monthly()` | `0 0 1 * *` | 1st of month at midnight | | `yearly()` / `annually()` | `0 0 1 1 *` | Jan 1 at midnight | | `onDays(days: number[])` | `0 0 * * {days}` | e.g. `onDays([1,3,5])` => `0 0 * * 1,3,5` | | `at(time: string)` | `{min} {hr} * * *` | e.g. `at('14:30')` => `30 14 * * *` | ### Configuration Methods (chainable, returns `this`) ```typescript setTimeZone(timezone: Timezone): this withErrorHandler(handler: CatchCallbackFn): this withMaxRuns(runs: number): this withProtection(callback?: (job: ScheduledJob) => void): this withName(name: string): this withContext(context: any): this withInterval(seconds: number): this between(startAt: string | Date, stopAt: string | Date): this withoutOverlapping(expiresAfterMinutes?: number): this onOneServer(): this runInBackground(): this ``` ### ScheduledJob Interface ```typescript interface ScheduledJob { stop: () => void nextRun: () => Date | null } ``` ### Timezone Type The `Timezone` type is a string union of ~70 IANA timezone identifiers (e.g., `'America/New_York'`, `'Europe/Berlin'`, `'Asia/Tokyo'`, `'UTC'`). Default is `'America/Los_Angeles'`. ### Internal Scheduling Engine - **Sub-minute** (`everySecond`): Uses `setInterval` with the configured `intervalMs`. - **Minute+**: Uses `parse()` from `@stacksjs/cron` to compute the next run time, then `setTimeout` to fire at the right moment. When the delay exceeds `2^31-1 ms` (~24.8 days), it chains shorter timeouts. - **Timezone-aware**: Converts "now" to the configured timezone via `toLocaleString()`, parses the cron pattern from that local time, then computes the real-world delay. - **maxRuns**: Tracks `runCount` and calls `stop()` when the limit is reached. - **Error handling**: If `options.catch` is set, errors are caught and passed to the handler instead of propagating. ### Overlap Prevention and Locking When `withoutOverlapping()` or `onOneServer()` is called: 1. Lock files are created in `storage/framework/locks/{taskName}.lock` 2. The lock directory is created automatically if missing 3. Lock files use exclusive write (`flag: 'wx'`) for atomicity 4. Stale locks expire after `overlapExpiresAfterMinutes` (default: 1440 = 24 hours) 5. Lock age is checked via file `mtime` 6. Locks are released in a `finally` block (or `.finally()` for async tasks) ### Background Execution When `runInBackground()` is called, the task is spawned as a detached child process via `node:child_process.spawn`. The child is `unref()`'d so it does not keep the parent alive. ## Helper Functions ```typescript import { sendAt, timeout } from '@stacksjs/scheduler' // Get the next Date a cron expression will fire sendAt(cronExpression: string | Date): Date | null // - String: delegates to parse() from @stacksjs/cron // - Date: returns the date if it's in the future, null otherwise // Get milliseconds until the next fire time timeout(cronExpression: string | Date): number // Returns -1 if no upcoming run ``` ## Cron Parser (@stacksjs/cron) ### parse() ```typescript import { parse } from '@stacksjs/cron' parse(expression: string, relativeDate?: Date | number): Date | null ``` - Uses `Bun.cron.parse()` when available (native Bun cron support), otherwise falls back to the built-in `parseCron()` implementation. - Returns the next matching UTC `Date`, or `null` if no match within ~4 years. - Throws on invalid expressions (wrong field count, out-of-range values). ### parseCron() (built-in parser) Supports the standard 5-field format: `minute hour dayOfMonth month dayOfWeek` **Operators**: `*` (all), `,` (list), `-` (range), `/` (step) **Named values**: `JAN`-`DEC`, `SUN`-`SAT` (case-insensitive, full names also accepted) **Nicknames**: `@yearly`, `@annually`, `@monthly`, `@weekly`, `@daily`, `@midnight`, `@hourly` **POSIX OR logic**: When both dayOfMonth and dayOfWeek are specified (neither is `*`), the expression matches when *either* condition is true. ### OS-Level Cron Registration ```typescript import { register, remove } from '@stacksjs/cron' // Register a persistent OS-level cron job (requires Bun.cron native support) await register(path: string, schedule: string, title: string) // Remove a registered cron job by title await remove(title: string) ``` These require Bun's native cron support (crontab on Linux, launchd on macOS, schtasks on Windows). The target script must export a `scheduled(controller)` handler. ## Every Enum (cron-jobs.ts) The `Every` enum maps human-readable intervals to cron expressions. Used in Job `rate` fields. ```typescript import { Every } from '@stacksjs/types' Every.Second // '* * * * * *' (6-field, sub-minute) Every.FiveSeconds // '*/5 * * * * *' Every.TenSeconds // '*/10 * * * * *' Every.ThirtySeconds // '*/30 * * * * *' Every.Minute // '* * * * *' Every.TwoMinutes // '*/2 * * * *' Every.FiveMinutes // '*/5 * * * *' Every.TenMinutes // '*/10 * * * *' Every.FifteenMinutes // '*/15 * * * *' Every.ThirtyMinutes // '*/30 * * * *' Every.Hour // '0 * * * *' Every.HalfHour // '0,30 * * * *' Every.Day // '0 0 * * *' Every.Week // '0 0 * * 0' Every.Weekday // '0 0 * * 1-5' Every.Weekend // '0 0 * * 0,6' Every.Month // '0 0 1 * *' Every.Year // '0 0 1 1 *' ``` ## runScheduler() (run.ts) The entry point for starting the scheduler process: 1. Globs `app/Jobs/*.ts` for job files 2. For each job with a `rate` property, maps the rate to a schedule method via `executeJobRate()` (switch on `Every.*` values) 3. Job names are derived from `job.name` or the filename, then `snakeCase()`'d 4. Imports and calls the default export from `app/Scheduler.ts` 5. Returns `Ok<string>` on success ```typescript import { runScheduler } from '@stacksjs/scheduler' const result = await runScheduler() ``` ## Queue-Level Scheduler (queue/src/scheduler.ts) A separate, queue-integrated scheduler that discovers jobs and dispatches them to the queue system: ```typescript import { startScheduler, stopScheduler, getSchedulerStatus, triggerJob } from '@stacksjs/queue' await startScheduler(config?: Partial<SchedulerConfig>) await stopScheduler() getSchedulerStatus(): { isRunning, jobCount, jobs[] } isSchedulerRunning(): boolean getRegisteredJobs(): Map<string, ScheduledJobState> await triggerJob(name: string) // Manually dispatch a scheduled job ``` ### SchedulerConfig ```typescript interface SchedulerConfig { checkInterval: number // ms between checks (default: 60000) timezone?: string preventOverlapping: boolean // default: true } ``` This scheduler polls on `checkInterval`, uses its own `shouldRunNow()` cron matcher (supports 5- and 6-field expressions), and dispatches jobs to the queue via `storeJob()` and `emitQueueEvent()`. ## app/Scheduler.ts User-defined scheduled tasks live here. Must export a default function: ```typescript import { schedule } from '@stacksjs/scheduler' export default function () { schedule.job('Inspire').hourly().setTimeZone('America/Los_Angeles') schedule.action('CleanupTempFiles').everyFiveMinutes() schedule.command('echo "maintenance"').daily() } // Graceful shutdown on SIGINT process.on('SIGINT', () => { schedule.gracefulShutdown().then(() => process.exit(0)) }) ``` ## CLI Commands - `buddy schedule:run` -- Runs `Action.ScheduleRun`, which calls `runScheduler()` - Options: `-p, --project [project]`, `--verbose` ## Code Examples ### Schedule a job with overlap prevention ```typescript schedule .job('ProcessPayments') .everyFiveMinutes() .withoutOverlapping(30) // Lock expires after 30 minutes .setTimeZone('America/New_York') .withErrorHandler((err) => console.error('Payment processing failed:', err)) ``` ### Schedule a command with max runs ```typescript schedule .command('bun run cleanup') .daily() .withMaxRuns(7) // Stop after 7 executions
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub