Recurring Jobs
Recurring jobs execute on a cron schedule. Jobly handles scheduling, deduplication, and execution history.
Register a Recurring Job
await recurringPublisher.AddOrUpdateRecurringJob(
new CleanupSessions(),
name: "session-cleanup",
cron: "0 * * * *"); // Every hour
AddOrUpdateRecurringJob only registers (or updates) the definition — it does not create a job. The RecurringJobScheduler background task creates jobs when the cron time arrives.
:::info Saves immediately
AddOrUpdateRecurringJob acquires a distributed lock on the job name and calls SaveChanges internally. You do not need to call SaveChanges after this method. The lock prevents race conditions when multiple app instances register the same recurring job concurrently.
:::
How It Works
- Registration:
AddOrUpdateRecurringJobstores the cron expression, message payload, and type. SetsNextExecutionto the next cron occurrence. - Scheduling:
RecurringJobSchedulerpolls every 15 seconds. WhenNextExecution <= now, it creates a job withScheduleTime = now(ready for immediate execution) and updatesNextExecutionto the next cron occurrence. - Deduplication: Before creating a new job, the scheduler checks the most recent
RecurringJobLogentry. If that job is stillEnqueuedorProcessing, it skips — no duplicate jobs. - Execution: The created job is a regular job. Workers pick it up, execute the handler, and it follows the normal lifecycle.
Execution History
Each job created by the scheduler is logged in RecurringJobLog. The dashboard shows execution history on the recurring job detail page — including jobs that have been cleaned up (shown as "Cleaned up").
The RecurringJobLog has a FK to Job with SET NULL cascade. When a job expires and is cleaned up, the log entry survives with JobId = null. The last 100 entries per recurring job are retained.
Cron Expressions
Standard 5-part cron (minute, hour, day, month, weekday) and 6-part with seconds:
* * * * * Every minute
0 * * * * Every hour
0 9 * * * Daily at 9 AM
0 0 * * 1 Every Monday at midnight
*/5 * * * * Every 5 minutes
0 9 * * 1-5 Weekdays at 9 AM
Manual Trigger
Trigger a recurring job immediately from the dashboard or via the API:
var svc = serviceProvider.GetRequiredService<IRecurringJobService>();
await svc.TriggerRecurringJob(id);
Enable / Disable
Disable a recurring job to temporarily stop it from creating new jobs. The scheduler still fires on schedule, but instead of creating a real job, it records a skipped entry in the execution history. This keeps the cron schedule in sync — when you re-enable, the job resumes from the next natural cron occurrence with no catchup burst.
POST /api/recurring/{id}/disable
POST /api/recurring/{id}/enable
Or use the Enable/Disable button on the dashboard.
How It Works
- Disable sets
DisabledAttimestamp on the recurring job - Scheduler still picks up the job when
NextExecution <= now, but seesDisabledAt != null - Instead of creating a job, it creates a
RecurringJobLogentry withSkipped = trueandJobId = null NextExecutionandLastExecutionadvance normally- Enable clears
DisabledAt— next cron tick creates a real job again
Behavior
| Scenario | What happens |
|---|---|
| Disable | Scheduler creates "Skipped" log entries instead of jobs |
| Enable | Next cron tick creates a real job as normal |
| Manual Trigger while disabled | Creates a real job — explicit trigger ignores disabled state |
AddOrUpdateRecurringJob while disabled | Updates the definition (cron, payload) but does not change disabled state |
Execution History
Skipped executions appear in the dashboard history with an orange Skipped badge, giving full visibility into what would have run. This is useful for auditing and confirming the schedule is correct before re-enabling.
Updating a Recurring Job
Call AddOrUpdateRecurringJob again with the same name. The cron expression, payload, and type are updated. NextExecution is recalculated.
// Change from hourly to every 30 minutes
await recurringPublisher.AddOrUpdateRecurringJob(
new CleanupSessions(),
name: "session-cleanup",
cron: "*/30 * * * *");
Deleting a Recurring Job
await recurringJobService.DeleteRecurringJob(id);
Or use the delete button on the dashboard.