Skip to content
Warlock.js v4

Events

The Scheduler class extends Node.js EventEmitter with a fully typed interface. Subscribe to events to add logging, metrics, alerting, or custom error handling without modifying job code.

import { scheduler } from "@warlock.js/scheduler";
scheduler.on("job:error", (name, error) => {
console.error(`[scheduler] ${name} failed:`, error);
});
scheduler.start();

Use .once() to handle an event only the first time it fires:

scheduler.once("scheduler:started", () => {
console.log("Scheduler is up and running");
});

Remove a listener with .off():

function handleError(name: string, error: unknown) { /* ... */ }
scheduler.on("job:error", handleError);
// later…
scheduler.off("job:error", handleError);

Fires immediately before a job’s callback is invoked.

scheduler.on("job:start", (jobName: string) => {
console.log(`[${new Date().toISOString()}] Starting: ${jobName}`);
});

Fires when a job finishes successfully. Includes the JobResult with timing and retry information.

import type { JobResult } from "@warlock.js/scheduler";
scheduler.on("job:complete", (jobName: string, result: JobResult) => {
console.log(`${jobName} finished in ${result.duration}ms`);
if (result.retries && result.retries > 0) {
console.log(` → succeeded after ${result.retries} retries`);
}
});

JobResult shape:

type JobResult = {
success: boolean;
duration: number; // milliseconds
error?: unknown; // present when success is false
retries?: number; // number of retry attempts made
};

Fires when a job’s callback throws and all retries are exhausted.

scheduler.on("job:error", (jobName: string, error: unknown) => {
// Send to your error tracker
Sentry.captureException(error, { tags: { job: jobName } });
});

Fires when a tick’s scheduled run cannot proceed because the job’s callback is already executing. Typically this happens when the job was kicked off outside the scheduler’s loop — a manual job.run(), a boot-time recovery sweep, another scheduler instance pointing at the same job — and the in-flight run is still going when the next scheduled tick arrives. See Overlap Prevention for the scenarios where this comes up.

scheduler.on("job:skip", (jobName: string, reason: string) => {
console.warn(`${jobName} skipped: ${reason}`);
});

Fires once when scheduler.start() is called and the tick loop begins.

scheduler.on("scheduler:started", () => {
console.log("Job scheduler started");
});

Fires when scheduler.stop() or scheduler.shutdown() stops the tick loop.

scheduler.on("scheduler:stopped", () => {
console.log("Job scheduler stopped");
});

Fires on every scheduler tick (once per second by default). Useful for debugging or heartbeat monitoring. Avoid heavy work inside this handler.

scheduler.on("scheduler:tick", (timestamp: Date) => {
// Fires every second — keep this handler lightweight
metrics.gauge("scheduler.tick", 1);
});
import type { SchedulerEvents } from "@warlock.js/scheduler";
// SchedulerEvents maps each event name to its argument tuple:
// {
// "job:start": [jobName: string];
// "job:complete": [jobName: string, result: JobResult];
// "job:error": [jobName: string, error: unknown];
// "job:skip": [jobName: string, reason: string];
// "scheduler:started": [];
// "scheduler:stopped": [];
// "scheduler:tick": [timestamp: Date];
// }

Production Pattern — Centralised Observability

Section titled “Production Pattern — Centralised Observability”

Set up all observers in one place before calling start():

import { scheduler } from "@warlock.js/scheduler";
import { logger } from "./logger";
import { metrics } from "./metrics";
import { alerts } from "./alerts";
// Timing metrics
scheduler.on("job:start", (name) => metrics.increment(`job.start.${name}`));
scheduler.on("job:complete", (name, result) => {
metrics.increment(`job.success.${name}`);
metrics.timing(`job.duration.${name}`, result.duration);
});
// Error alerting
scheduler.on("job:error", (name, error) => {
logger.error({ job: name, error }, "Job failed");
alerts.critical(`Scheduler job "${name}" failed`, error);
});
// Skipped-run visibility
scheduler.on("job:skip", (name, reason) => {
logger.warn({ job: name, reason }, "Job skipped");
});
// Lifecycle
scheduler.on("scheduler:started", () => logger.info("Scheduler started"));
scheduler.on("scheduler:stopped", () => logger.info("Scheduler stopped"));
// Register jobs and start
scheduler
.addJob(/* ... */)
.start();