Configuration
The Scheduler class is configurable through a set of chainable methods. All configuration must happen before calling .start().
Tick Interval — runEvery()
Section titled “Tick Interval — runEvery()”The scheduler checks for due jobs on a recurring tick. The default tick interval is 1000 ms (1 second). Increase it to reduce CPU overhead when your shortest job interval is longer than a second:
The tick interval is drift-compensated — if a tick’s work takes 200 ms, the next tick fires 800 ms later (not 1000 ms), so the cadence between tick starts averages the configured interval rather than interval + work-time. Long-running jobs still block the next tick (the scheduler is sequential around its loop), but short ticks don’t slowly drift behind the wall clock.
import { Scheduler, job } from "@warlock.js/scheduler";
const myScheduler = new Scheduler();
myScheduler .runEvery(60_000) // check for due jobs once per minute .addJob(job("report", generateReport).daily().at("09:00")) .addJob(job("cleanup", cleanupFn).weekly().on("sunday").at("02:00")) .start();The minimum allowed interval is 100 ms. Smaller values throw an error:
myScheduler.runEvery(50); // throws: "Tick interval must be at least 100ms"Parallel Execution — runInParallel()
Section titled “Parallel Execution — runInParallel()”By default, jobs that are due at the same tick run sequentially (one after the other). Enable parallel mode to run them concurrently:
import { Scheduler, job } from "@warlock.js/scheduler";
const myScheduler = new Scheduler();
myScheduler .runInParallel(true) // enable parallel execution .addJob(job("job-a", taskA).everyHour()) .addJob(job("job-b", taskB).everyHour()) .addJob(job("job-c", taskC).everyHour()) .start();All three jobs will start simultaneously on each hourly tick.
Concurrency Limit
Section titled “Concurrency Limit”Pass a second argument to cap how many jobs run at once:
myScheduler.runInParallel(true, 5); // at most 5 concurrent jobs per tickWhen more jobs are due than the concurrency limit allows, they are batched: the first 5 start together, the next batch starts after the first finishes, and so on.
The default max concurrency is 10.
Job Management
Section titled “Job Management”Adding Jobs
Section titled “Adding Jobs”import { scheduler, job } from "@warlock.js/scheduler";
// Add a single jobscheduler.addJob(job("cleanup", cleanupFn).daily().at("03:00"));
// Add multiple jobs at oncescheduler.addJobs([ job("job-a", taskA).everyHour(), job("job-b", taskB).daily(), job("job-c", taskC).weekly(),]);
// Fluent chainingscheduler .addJob(job("a", taskA).everyMinutes(5)) .addJob(job("b", taskB).everyMinutes(10));Creating Jobs Inline — newJob()
Section titled “Creating Jobs Inline — newJob()”newJob() creates, registers, and returns a job in one call:
scheduler .newJob("cleanup", cleanupFn) .daily() .at("03:00");Removing Jobs
Section titled “Removing Jobs”const removed = scheduler.removeJob("cleanup"); // true if found and removedLooking Up Jobs
Section titled “Looking Up Jobs”const j = scheduler.getJob("cleanup");
if (j) { console.log("Next run:", j.nextRun?.toISOString()); console.log("Is running:", j.isRunning);}Listing All Jobs
Section titled “Listing All Jobs”const all = scheduler.list(); // readonly Job[]console.log(`${scheduler.jobCount} jobs registered`);Lifecycle Methods
Section titled “Lifecycle Methods”start()
Section titled “start()”Prepares all registered jobs and begins the tick loop. Throws if the scheduler is already running or has no jobs:
scheduler.start();// throws "Cannot start scheduler with no jobs." if addJob() was never called// throws "Scheduler is already running." if called twicestop()
Section titled “stop()”Immediately halts the tick loop. Any jobs currently executing are not waited on:
scheduler.stop();// scheduler.isRunning === falseCalling stop() on a scheduler that was never started (or was already stopped) is a safe no-op — no scheduler:stopped event is emitted in that case.
shutdown(timeout?)
Section titled “shutdown(timeout?)”Gracefully stops the scheduler. It:
- Stops scheduling new ticks
- Waits for all currently-running jobs to finish
- Times out after the specified duration (default 30 000 ms)
// Default 30-second timeoutawait scheduler.shutdown();
// Custom timeoutawait scheduler.shutdown(60_000); // wait up to 60 sWire this to your process signal handlers:
import { scheduler } from "@warlock.js/scheduler";
scheduler.start();
process.on("SIGTERM", async () => { await scheduler.shutdown(30_000); process.exit(0);});
process.on("SIGINT", async () => { await scheduler.shutdown(5_000); process.exit(0);});Checking Scheduler State
Section titled “Checking Scheduler State”console.log(scheduler.isRunning); // booleanconsole.log(scheduler.jobCount); // number of registered jobsCronParser Utility
Section titled “CronParser Utility”The CronParser class and parseCron() helper are exported for use outside of scheduling contexts — for example, to preview the next run time of a cron expression:
import { parseCron, CronParser } from "@warlock.js/scheduler";import type { CronFields } from "@warlock.js/scheduler";
// Quick utilityconst parser = parseCron("0 9 * * 1-5");const nextRun = parser.nextRun();console.log("Next:", nextRun.toISOString());
// Check if right now matchesconsole.log("Matches now:", parser.matches(dayjs()));
// Inspect the parsed fieldsconst fields: CronFields = parser.fields;// { minutes: [0], hours: [9], daysOfMonth: [1..31], months: [1..12], daysOfWeek: [1,2,3,4,5] }
// Access the original expressionconsole.log(parser.expression); // "0 9 * * 1-5"Complete Setup Example
Section titled “Complete Setup Example”import { Scheduler, job } from "@warlock.js/scheduler";import { logger } from "./logger";
const appScheduler = new Scheduler();
// ObservabilityappScheduler.on("job:start", (name) => logger.debug(`Job started: ${name}`));appScheduler.on("job:complete", (name, result) => logger.info({ name, duration: result.duration }, "Job complete"));appScheduler.on("job:error", (name, error) => logger.error({ name, error }, "Job failed"));appScheduler.on("job:skip", (name, reason) => logger.warn({ name, reason }, "Job skipped"));
// ConfigurationappScheduler .runEvery(1_000) // check every second .runInParallel(true, 5) // up to 5 concurrent jobs
// Jobs .addJob( job("token-cleanup", deleteExpiredTokens) .daily() .at("03:00") .preventOverlap() .retry(3, 2000) ) .addJob( job("weekly-report", generateWeeklyReport) .weekly() .on("monday") .at("08:00") .inTimezone("America/New_York") .preventOverlap() ) .addJob( job("heartbeat", pingExternalService) .everyMinutes(5) .retry(2, 1000, 2) );
appScheduler.start();logger.info("Scheduler started with", appScheduler.jobCount, "jobs");
// Graceful shutdownprocess.on("SIGTERM", async () => { logger.info("Shutting down scheduler…"); await appScheduler.shutdown(30_000); logger.info("Scheduler shut down cleanly"); process.exit(0);});