Ditsmod schedule
Scheduling tasks in Ditsmod with @ditsmod/schedule: @cron / @interval / @timeout decorators, CronExpression enum, CronOptions, SchedulerRegistry (query/start/stop/delete tasks at runtime), ScheduleModule setup, provider scope constraints (only providersPerMod / providersPerApp), graceful shutdown behavior, and dynamic task management patterns.From its SKILL.md
npx -y skills add ditsmod/agent-skills --skill ditsmod-scheduleAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
16.2 KB, ~3.7k tokens by cl100k_base, as published. Nobody here has run it
Ditsmod Schedule
@ditsmod/schedule provides decorator-based task scheduling — cron jobs, periodic intervals, and one-shot timeouts — for Ditsmod applications. Scheduling is implemented as a Ditsmod extension (ScheduleExtension) and requires no manual wiring beyond importing ScheduleModule.
Installation
yarn add @ditsmod/schedule
Module Setup
Import ScheduleModule in any root or feature module. Register task classes in providersPerMod (or providersPerApp) of that same module — not in providersPerRou or providersPerReq.
import { rootModule } from '@ditsmod/core';
import { ScheduleModule } from '@ditsmod/schedule';
import { ReportTasks } from './report-tasks.service.js';
import { CleanupTasks } from './cleanup-tasks.service.js';
@rootModule({
imports: [ScheduleModule],
providersPerMod: [ReportTasks, CleanupTasks],
})
export class AppModule {}
[!IMPORTANT] Task classes must be registered in
providersPerModorprovidersPerApp. Registration inprovidersPerRouorprovidersPerReqis explicitly forbidden — the extension will emit a warning and skip those providers. See Provider Scope Constraints for details.
Declaring Tasks
Create an @injectable() service and decorate its methods with @cron, @interval, or @timeout. Each decorator attaches metadata that the ScheduleExtension reads during bootstrap.
import { injectable, Logger } from '@ditsmod/core';
import { cron, interval, timeout, CronExpression } from '@ditsmod/schedule';
@injectable()
export class ReportTasks {
constructor(private logger: Logger) {}
@cron(CronExpression.EVERY_DAY_AT_MIDNIGHT, { name: 'daily-report' })
generateDailyReport(): void {
this.logger.log('info', 'Generating daily report...');
}
@interval('cache-refresh', 30_000)
refreshCache(): void {
this.logger.log('info', 'Refreshing cache...');
}
@timeout('startup-warmup', 5_000)
warmUp(): void {
this.logger.log('info', 'Post-startup warm-up complete.');
}
}
Decorator Reference
@cron(cronTime, options?)
Runs the method on a cron schedule. Uses the cron package under the hood.
| Parameter | Type | Description |
|---|---|---|
cronTime | string | Date | DateTime | Standard 5- or 6-field cron expression, or a Date |
options | CronOptions (optional) | See table below |
CronOptions
| Property | Type | Default | Description |
|---|---|---|---|
name | string | uuid | Unique name. Required to retrieve the job via SchedulerRegistry.getCronJob(name). |
timeZone | string | — | IANA timezone string (e.g., 'America/New_York'). Mutually exclusive with utcOffset. |
utcOffset | number | — | UTC offset in minutes. Alternative to timeZone. |
unrefTimeout | boolean | false | When true, the underlying timeout is unreffed so the process can exit naturally. |
waitForCompletion | boolean | false | When true, prevents overlapping executions — waits for the previous tick to finish. |
disabled | boolean | false | When true, the job is registered but not started automatically. |
threshold | number | 250 | Millisecond threshold to skip missed ticks on slow/busy hardware. |
initialDelay | number | — | Delay in milliseconds before the cron job first starts after bootstrap. |
6-field cron syntax (seconds-level precision):
┌────────────── second (0-59) [optional]
│ ┌─────────── minute (0-59)
│ │ ┌──────── hour (0-23)
│ │ │ ┌───── day of month (1-31)
│ │ │ │ ┌── month (1-12)
│ │ │ │ │ ┌─ day of week (0-7, 0 and 7 = Sunday)
* * * * * *
@cron('*/30 * * * * *', { name: 'every-30-seconds' }) // every 30 seconds (6 fields)
@cron('0 9 * * 1-5', { name: 'weekday-9am' }) // Mon-Fri at 09:00 (5 fields)
@cron(new Date('2027-01-01T00:00:00Z'), { name: 'new-year' })
@interval(nameOrTimeout: string | number, timeout?: number)
Runs the method repeatedly every timeout milliseconds via setInterval. If only nameOrTimeout is provided as a number, it acts as the timeout (anonymous task).
// Signature overloads:
@interval(timeout: number)
@interval(name: string, timeout: number)
// Usage examples:
@interval(3000) // anonymous, fires every 3 s
@interval('cache-refresh', 3000) // named, fires every 3 s
- When no name is provided, the interval is tracked internally with a random UUID and cannot be retrieved by name via
SchedulerRegistry. - Named intervals can be stopped at runtime with
SchedulerRegistry.deleteInterval(name).
@timeout(nameOrTimeout: string | number, timeout?: number)
Runs the method once after timeout milliseconds via setTimeout. If only nameOrTimeout is provided as a number, it acts as the timeout (anonymous task).
// Signature overloads:
@timeout(timeout: number)
@timeout(name: string, timeout: number)
// Usage examples:
@timeout(5_000) // anonymous, fires once after 5 s
@timeout('startup-warmup', 5_000) // named
- Named timeouts can be cancelled at runtime with
SchedulerRegistry.deleteTimeout(name).
CronExpression Enum
CronExpression provides a curated set of named expressions to avoid hard-coding cron strings:
import { CronExpression } from '@ditsmod/schedule';
@cron(CronExpression.EVERY_5_SECONDS)
@cron(CronExpression.EVERY_HOUR)
@cron(CronExpression.EVERY_DAY_AT_MIDNIGHT)
@cron(CronExpression.EVERY_WEEKDAY)
@cron(CronExpression.MONDAY_TO_FRIDAY_AT_9AM)
Key members (supports both 5-field standard and 6-field seconds-precision expressions):
| Member | Cron String |
|---|---|
EVERY_SECOND | * * * * * * |
EVERY_5_SECONDS | */5 * * * * * |
EVERY_MINUTE | */1 * * * * |
EVERY_HOUR | 0 0-23/1 * * * |
EVERY_DAY_AT_MIDNIGHT | 0 0 * * * |
EVERY_DAY_AT_NOON | 0 12 * * * |
EVERY_WEEKDAY | 0 0 * * 1-5 |
EVERY_WEEKEND | 0 0 * * 6,0 |
EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT | 0 0 1 * * |
EVERY_QUARTER | 0 0 1 */3 * |
EVERY_YEAR | 0 0 1 1 * |
MONDAY_TO_FRIDAY_AT_9AM | 0 0 09 * * 1-5 |
EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM | 0 */30 9-17 * * * |
SchedulerRegistry — Runtime Task Management
SchedulerRegistry is registered at application scope (providersPerApp) and is injectable anywhere in the application — services, controllers, etc.
import { injectable } from '@ditsmod/core';
import { SchedulerRegistry } from '@ditsmod/schedule';
@injectable()
export class TaskManagerService {
constructor(private registry: SchedulerRegistry) {}
listAll() {
return {
cronJobs: Array.from(this.registry.getCronJobs().keys()),
intervals: this.registry.getIntervals(),
timeouts: this.registry.getTimeouts(),
};
}
stopCron(name: string): void {
if (this.registry.doesExist('cron', name)) {
this.registry.deleteCronJob(name); // stops and removes
}
}
pauseAndResumeCron(name: string): void {
const job = this.registry.getCronJob(name); // throws if not found
void job.stop();
// ... later:
job.start();
}
stopInterval(name: string): void {
if (this.registry.doesExist('interval', name)) {
this.registry.deleteInterval(name); // calls clearInterval internally
}
}
cancelTimeout(name: string): void {
if (this.registry.doesExist('timeout', name)) {
this.registry.deleteTimeout(name); // calls clearTimeout internally
}
}
}
SchedulerRegistry API
| Method | Description |
|---|---|
doesExist(type, name) | Returns true if a task of the given type with that name is registered. |
getCronJob(name): CronJob | Returns the raw CronJob instance (from the cron package). Throws if not found. |
getCronJobs(): Map<string, CronJob> | Returns all registered cron jobs. |
deleteCronJob(name): void | Stops the cron job and removes it from the registry. Throws if not found. |
getIntervals(): string[] | Returns names of all registered intervals. |
getInterval(name): any | Returns the raw interval handle. Throws if not found. |
deleteInterval(name): void | Calls clearInterval and removes the entry. Throws if not found. |
getTimeouts(): string[] | Returns names of all registered timeouts. |
getTimeout(name): any | Returns the raw timeout handle. Throws if not found. |
deleteTimeout(name): void | Calls clearTimeout and removes the entry. Throws if not found. |
addCronJob(name, job): void | Manually registers a pre-built CronJob. Use for programmatic job creation. |
addInterval(name, intervalId): void | Manually registers a raw interval handle. |
addTimeout(name, timeoutId): void | Manually registers a raw timeout handle. |
[!TIP] Always call
doesExist()beforegetCronJob()/getInterval()/getTimeout()when the existence of the task is not guaranteed — the getters throw anErrorif the name is not found.
Programmatic Task Registration Example
If you need to dynamically create and register a task at runtime (rather than using class decorators):
import { CronJob } from 'cron';
// Inside a service or controller method:
const customJob = new CronJob('* * * * *', () => {
console.log('Running programmatic cron job...');
});
this.registry.addCronJob('dynamic-cron', customJob);
customJob.start();
Graceful Shutdown
SchedulerOrchestrator implements the Ditsmod BeforeShutdown lifecycle hook. On application shutdown it automatically:
- Clears all pending
initialDelaytimers. - Stops and deletes all registered cron jobs.
- Clears and deletes all intervals.
- Clears and deletes all timeouts.
No manual cleanup is required in application code.
Disabled Jobs and initialDelay
Use disabled: true to register a cron job without starting it. Start it manually when ready:
@cron(CronExpression.EVERY_HOUR, { name: 'heavy-report', disabled: true })
runHeavyReport(): void { /* ... */ }
// Elsewhere, e.g. triggered by a route or admin action:
const job = this.registry.getCronJob('heavy-report');
job.start();
Use initialDelay to defer the first execution after bootstrap:
@cron(CronExpression.EVERY_5_MINUTES, {
name: 'delayed-poller',
initialDelay: 10_000, // starts only after 10 seconds post-bootstrap
})
pollExternalApi(): void { /* ... */ }
[!IMPORTANT] If
disabled: trueis combined withinitialDelay, the job is registered but never started automatically —initialDelayis silently ignored in that case.
Provider Scope Constraints
Scheduled tasks are application-lifetime singletons that must be instantiated once, not per-route or per-request.
| Provider scope | Scheduling supported? | Notes |
|---|---|---|
providersPerApp | Yes | Scanned only in the module where isLastModule === true during extension stage1. |
providersPerMod | Yes | Scanned in the module that imports ScheduleModule. |
providersPerRou | No — warning logged | Tasks cannot be route-scoped singletons. |
providersPerReq | No — warning logged | Tasks cannot be request-scoped singletons. |
[!IMPORTANT]
SchedulerRegistryandSchedulerOrchestratorare registered atprovidersPerApp. All tasks from all modules share a single registry and are all cleaned up together on shutdown.
Internals: Bootstrap Lifecycle
Understanding how ScheduleExtension operates helps avoid subtle bugs:
stage1— scansprovidersPerMod(andprovidersPerAppfor the last module) for classes that have@cron,@interval, or@timeoutmethod decorators. Emits warnings for route/request-scoped classes.stage2— receives the ready module injector. Resolves each scanned class via the injector, reads its decorator metadata viaReflector.collectMeta(), and registers each decorated method withSchedulerOrchestrator(which stores them as pending).stage3— callsSchedulerOrchestrator.mountJobs(), which activates all pending timeouts (setTimeout), intervals (setInterval), and cron jobs (CronJob.from(...)). This runs once for the entire application.
The ScheduleExtension is registered with exportOnly: true in ScheduleModule, meaning it runs only in the modules that import ScheduleModule, not in ScheduleModule itself.
Troubleshooting
| Symptom | Cause / Fix |
|---|---|
| Task method never executes | Class not registered in providersPerMod or providersPerApp of a module that imports ScheduleModule. |
Warning: Cannot register schedule on "Foo@bar"... | Task class is in providersPerRou or providersPerReq. Move it to providersPerMod. |
getCronJob(name) throws No Scheduler found | Name not provided in CronOptions, or the task was already deleted. Use doesExist() first. |
addCronJob(name, job) throws Duplicate Scheduler | Two decorated methods share the same name. Each name must be unique across the entire application. |
| Cron job not stopped on shutdown | Ensure Ditsmod shutdown hooks are properly triggered. SchedulerOrchestrator.beforeShutdown() handles cleanup. |
initialDelay not working | Check that disabled is not also set to true — combining both prevents automatic start. |
| Anonymous tasks not retrievable by name | Tasks without a name are assigned a random UUID at runtime. Always provide a name for tasks you need to manage. |
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.