Skip to main content

Creating Scheduled Jobs

Scheduled jobs in SolidX allow you to run recurring tasks such as sending notifications, cleaning up records, syncing data, or performing regular maintenance.

This section walks you through how to create and integrate custom scheduled jobs into your application.

For the full metadata schema reference, see Scheduled Jobs Metadata.

Adding a New Scheduled Job

Follow these steps to define and use a custom scheduled job:

1. Create a Job Service

Create a new service class that implements the IScheduledJob interface and decorate it with @ScheduledJobProvider().

Example: Late Fee Calculator Job
import { Injectable, Logger } from '@nestjs/common';
import { IScheduledJob, ScheduledJob, ScheduledJobProvider } from '@solidxai/core';
import { FeesService } from '../fees/fees.service';

@Injectable()
@ScheduledJobProvider()
export class LateFeeCalculatorJob implements IScheduledJob {
private readonly logger = new Logger(LateFeeCalculatorJob.name);

constructor(private readonly feesService: FeesService) {}

async execute(job: ScheduledJob): Promise<void> {
this.logger.log(`Running job: ${job.scheduleName}`);
try {
const overdueAccounts = await this.feesService.findOverdue();
for (const account of overdueAccounts) {
await this.feesService.applyLateFee(account);
}
this.logger.log(`Late fees applied to ${overdueAccounts.length} accounts`);
} catch (err) {
this.logger.error('Late fee calculation failed', err.stack);
}
}
}

2. Register the Service

Add the job class to the providers array of its NestJS module so the framework can inject and resolve it.

3. Define the Job in Metadata

Add an entry to the scheduledJobs array in your module metadata file: solid-api/module-metadata/{module-name}/{module-name}-metadata.json

Example: Daily job with a time window
{
"scheduledJobs": [
{
"scheduleName": "Late Fee Calculation",
"isActive": true,
"frequency": "Daily",
"startTime": "08:00:00",
"endTime": "18:00:00",
"startDate": null,
"endDate": null,
"dayOfMonth": null,
"job": "LateFeeCalculatorJob",
"moduleUserKey": "fees-portal"
}
]
}

This runs LateFeeCalculatorJob every day (including weekends — dayOfWeek is only enforced for Weekly frequency). The startTime/endTime window means the scheduler will hold off executing the job until 08:00 if it comes due earlier, and will skip it entirely if it comes due after 18:00 (waiting for the next day's window to open). No date range is set, so the job runs indefinitely.

Example: Weekly job on specific days
{
"scheduledJobs": [
{
"scheduleName": "Weekly Overdue Report",
"isActive": true,
"frequency": "Weekly",
"startTime": "07:00:00",
"endTime": null,
"startDate": null,
"endDate": null,
"dayOfMonth": null,
"dayOfWeek": "[\"Monday\",\"Thursday\"]",
"job": "OverdueReportJob",
"moduleUserKey": "fees-portal"
}
]
}

This runs OverdueReportJob once every 7 days, but only if the due date falls on a Monday or Thursday — if it doesn't, the scheduler skips it every minute until an eligible day arrives, then runs and resets the 7-day clock from that point. It will not execute before 07:00, and has no upper time boundary. No date range is set, so it runs indefinitely.


Configuration Reference

PropertyTypeDescription
scheduleNamestringHuman-readable name for the job
isActivebooleanWhether the job is enabled. Defaults to false
frequencystringHow often the job runs — see Supported Frequencies
cronExpressionstringRequired when frequency is "Custom" — a valid cron expression
startTimestring | nullEarliest time of day to run, in HH:MM:SS format
endTimestring | nullLatest time of day to run, in HH:MM:SS format
startDatestring | nullISO date — job will not run before this date
endDatestring | nullISO date — job will not run after this date
dayOfWeekstringStringified JSON array of day names, e.g. "[\"Monday\",\"Friday\"]"
dayOfMonthnumber | nullDay of the month for Monthly jobs (1–31)
jobstringExact class name of the job implementation (case-sensitive)
moduleUserKeystringUser key of the module this job belongs to

Supported Frequencies

ValueInterval
Every MinuteEvery 60 seconds
HourlyEvery 60 minutes
DailyEvery 24 hours
WeeklyEvery 7 days, filtered by dayOfWeek
MonthlySame day next month, filtered by dayOfMonth
CustomArbitrary schedule defined by a cron expression

Custom Cron Expressions

When the built-in frequencies aren't precise enough, set frequency to "Custom" and provide a standard cron expression in the cronExpression field.

{
"scheduleName": "Bi-hourly Sync",
"isActive": true,
"frequency": "Custom",
"cronExpression": "0 */2 * * *",
"job": "DataSyncJob",
"moduleUserKey": "my-module"
}

Notes:

  • Cron expressions are evaluated in UTC.
  • The minimum allowed interval is 1 minute. Expressions that resolve to a shorter interval are rejected and the job falls back to a daily schedule.
  • If the expression is invalid or missing, the platform logs an error and falls back to a 24-hour interval.

Info

Use a tool like crontab.guru to build and validate your cron expressions before adding them to metadata.


How It Works

The SolidX scheduler runs an internal polling loop every minute (powered by @Cron(CronExpression.EVERY_MINUTE) under the hood). On each tick it:

  1. Fetches due jobs — queries all active jobs where nextRunAt ≤ now or nextRunAt is null (newly created jobs that haven't run yet).
  2. Applies scheduling gates — each job is checked against startDate/endDate (date range), startTime/endTime (time-of-day window), dayOfWeek (for Weekly), and dayOfMonth (for Monthly) before being allowed to execute.
  3. Guards against overlap — if a job is still running from a previous tick it is skipped, preventing concurrent duplicate executions.
  4. Dispatches execution — the job's execute(job) method is called with the full ScheduledJob entity, giving the handler access to all metadata.
  5. Updates tracking fields — after a successful run, lastRunAt is stamped and nextRunAt is computed based on the configured frequency, so the scheduler knows exactly when to pick it up next.

Environment Control

Set SOLID_SCHEDULER_ENABLED=false in your environment to disable the scheduler entirely (useful for CLI contexts or environments where background jobs should not run).