Skip to content

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 TimeZoneId or an invalid Cron fails 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

  1. RecurringJobSchedulerService polls every PollingInterval (default: 15s)
  2. Finds recurring jobs where NextExecution <= UtcNow
  3. Acquires a distributed lock to prevent duplicate firings across nodes
  4. Creates a new JobRecord and enqueues it
  5. 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