Recurring Jobs¶
Schedule jobs to run on a cron expression. Each firing creates a new JobRecord — recurring jobs are schedules, not persistent executions.
Code-Based Configuration¶
Simple Recurring Job¶
builder.Services.AddNexJob(options =>
{
// Run cleanup every day at 2 AM
options.AddRecurringJob<CleanupOldLogsJob>(
id: "cleanup-daily",
cron: "0 2 * * *");
});
With Input¶
builder.Services.AddNexJob(options =>
{
options.AddRecurringJob<GenerateReportJob, ReportInput>(
id: "weekly-report",
cron: "0 9 * * 1", // Monday 9 AM
input: new ReportInput("weekly"));
});
With Time Zone¶
builder.Services.AddNexJob(options =>
{
options.AddRecurringJob<SendDailyDigestJob>(
id: "daily-digest",
cron: "0 8 * * *",
timeZoneId: "America/New_York");
});
With Queue¶
builder.Services.AddNexJob(options =>
{
options.AddRecurringJob<HeavyAnalyticsJob>(
id: "analytics-hourly",
cron: "0 * * * *",
queue: "compute");
});
Configuration via appsettings.json¶
Define recurring jobs in configuration instead of code.
{
"NexJob": {
"RecurringJobs": [
{
"Job": "CleanupOldLogsJob",
"Cron": "0 2 * * *",
"TimeZoneId": "America/New_York",
"Queue": "default"
},
{
"Job": "GenerateReportJob",
"Input": "{\"ReportType\": \"weekly\"}",
"Cron": "0 9 * * 1",
"Queue": "reports"
}
]
}
}
Register with configuration:
builder.Services.AddNexJob(builder.Configuration);
Or combined with code options:
builder.Services.AddNexJob(builder.Configuration, options =>
{
options.Workers = 20;
});
Rules:
- The Job field must match the class name (not fully qualified). If two jobs share a name, give each entry an explicit Id.
- Input is a JSON string (escape the quotes), not a nested JSON object. It is validated and deserialized using the job's IJob<T> input type; omit it for an IJob without input.
- Each entry can also set Id, Queue (default default), TimeZoneId, ConcurrencyPolicy (SkipIfRunning by default) and Enabled (default true).
What happens on every start¶
The configuration is applied on every start, not only the first one: if you change the Cron, Queue, Input or TimeZoneId of an entry, the stored schedule follows the file after the next restart.
- What an operator changed in the dashboard is kept: a cron override, a paused job and a deleted job stay as they are.
- The first run of a job that is registered (or whose definition is re-applied) happens at its next cron occurrence. It does not fire immediately on start, so restarting the application never triggers every configured job at once.
- An unknown
TimeZoneIdor an invalidCronfails the registration of that entry only. The error is logged and nothing is stored for it.
Concurrency Policy¶
Control what happens when a recurring job fires while the previous instance is still running.
builder.Services.AddNexJob(options =>
{
// Default: skip if the previous instance is still running
options.AddRecurringJob<SlowSyncJob>(
id: "slow-sync",
cron: "*/5 * * * *",
concurrencyPolicy: RecurringConcurrencyPolicy.SkipIfRunning);
// Allow concurrent executions
options.AddRecurringJob<IndependentTaskJob>(
id: "independent-task",
cron: "0 * * * *",
concurrencyPolicy: RecurringConcurrencyPolicy.AllowConcurrent);
});
| Policy | Behavior |
|---|---|
SkipIfRunning (default) |
Uses idempotency key recurring:{Id} — skips if previous instance is still active |
AllowConcurrent |
No deduplication — each firing creates a new JobRecord |
Removing Recurring Jobs¶
await scheduler.RemoveRecurringAsync("cleanup-daily", ct);
This removes the schedule — it does not affect already-created JobRecord instances.
You can also create or update a schedule at runtime with scheduler.RecurringAsync<TJob>(recurringJobId, cron, ...); note that this method names the id parameter recurringJobId, while the options.AddRecurringJob extension used at startup names it id.
How It Works¶
RecurringJobSchedulerServicepolls everyPollingInterval(default: 15s)- Finds recurring jobs where
NextExecution <= UtcNow - Acquires a distributed lock to prevent duplicate firings across nodes
- Creates a new
JobRecordand enqueues it - Calculates the next execution time
Implication: Each firing is an independent job with its own lifecycle, retries, and dead-letter handling. If one firing fails, it does not affect the next scheduled firing.
Next Steps¶
- Continuations — Chain jobs after each other
- Configuration Reference — All recurring options
- Dashboard — Monitor recurring job executions