Skip to main content

Scheduled Jobs (Cron Jobs)

Bellamy Book uses Quartz.NET for in-process scheduled jobs. Most jobs run inside the main API process; one scheduled job runs inside the BlogAutoGenerationWorker (separate service). Schedules are configured via cron expressions in appsettings.json (or environment) and, where noted, can be overridden in the Admin Panel or database.

Cron format

  • Quartz uses a 6- or 7-field format: second minute hour day-of-month month day-of-week [year].
  • Example: 0 0 2 * * ? = every day at 2:00 AM (UTC).
  • The backup schedule also accepts Unix-style 5-field (minute hour day month day-of-week); the API converts it to Quartz format.

Config keys use the pattern Quartz:{JobName} (e.g. Quartz:RefreshTokenCleanupJob).


Jobs in the main API

All of the following jobs are registered in the API project (Src/backend/API) and run in the same process as the web API.

JobConfig keyDefault scheduleDescription
RefreshTokenCleanupJobQuartz:RefreshTokenCleanupJob0 0 * * * ? (daily at midnight)Deletes expired refresh tokens from the database.
UserSyncJobQuartz:UserSyncJob0 */5 * * * ? (every 5 minutes)Syncs user data from PostgreSQL to Neo4j (social graph). Skips when a Neo4j restore is in progress.
ElasticsearchSyncJobQuartz:ElasticsearchSyncJob0 */15 * * * ? (every 15 minutes)Catch-up sync of users, posts, blogs, and hashtags from PostgreSQL to Elasticsearch/OpenSearch. Catches missed events when event-driven sync fails.
TempFileCleanupJobQuartz:TempFileCleanupJob0 0 */6 * * ? (every 6 hours)Deletes temporary files in the MinIO temp bucket older than 24 hours.
StoryCleanupJobQuartz:StoryCleanupJob0 0 * * * ? (every hour)Archives expired stories (e.g. older than 24 hours). Clears story-related caches after cleanup.
BirthdayNotificationJobQuartz:BirthdayNotificationJob0 0 0 * * ? (daily at midnight UTC)Finds users with birthdays today; sends "Happy Birthday" to them and "Friend Birthday" to their friends via the notification queue.
EmailCampaignJobQuartz:EmailCampaignJob0 * * ? * * (every minute)Picks campaigns that are ready to send (Scheduled/Draft with ScheduledAt in the past), sends queued emails in batches (e.g. 50 per run). See Email Campaigns.
PollExpirationJobQuartz:PollExpirationJob0 */15 * * * ? (every 15 minutes)Marks expired polls as expired; invalidates related cache.
LicenseValidationJobQuartz:LicenseValidationJob0 0 0 ? * MON (every Monday at midnight)Validates the license with the main server (CONSUMER mode only; skipped in MAIN mode).
ScheduledBackupJobQuartz:ScheduledBackupJob or Backup:Schedule0 0 2 * * ? (daily at 2 AM)Starts a scheduled backup if backup is enabled in config. Schedule can be changed in Admin Panel → Backup → Config; supports Unix 5-field or Quartz 6-field cron.
SampleJobQuartz:SampleJob0 * * ? * * (every minute)Example/demo job (e.g. logs or simulates work). Can be disabled or removed in production.
BackgroundJobSampleQuartz:BackgroundJobSample*/5 * * * * ? (every 5 seconds)Example/demo job. Can be disabled or removed in production.

Job in BlogAutoGenerationWorker

The BlogAutoGenerationWorker (Src/backend/BlogAutoGenerationWorker) is a separate process and has its own Quartz scheduler with one job:

JobSchedule sourceDefaultDescription
BlogGenerationJobDatabase (AI Agent settings) or config fallback AiAgentSettings:ScheduleCronExpression0 0 2 * * ? (daily at 2 AM)Runs when AI blog generation is enabled; generates blogs per admin-configured topics/blogs-per-day. Schedule is configurable in Admin Panel → Settings → AI Agent. Reschedule requests can be sent via RabbitMQ when admin changes the cron.

See AI Blog Generation.


Configuration example

In appsettings.json (or environment variables) for the API project:

{
"Quartz": {
"RefreshTokenCleanupJob": "0 0 * * * ?",
"UserSyncJob": "0 */5 * * * ?",
"ElasticsearchSyncJob": "0 */15 * * * ?",
"TempFileCleanupJob": "0 0 */6 * * ?",
"StoryCleanupJob": "0 0 * * * ?",
"BirthdayNotificationJob": "0 0 0 * * ?",
"EmailCampaignJob": "0 * * ? * *",
"PollExpirationJob": "0 */15 * * * ?",
"LicenseValidationJob": "0 0 0 ? * MON",
"ScheduledBackupJob": "0 0 2 * * ?"
},
"Backup": {
"Schedule": "0 0 2 * * ?"
}
}

Backup schedule can also be set in Admin Panel → Backup → Configuration; the API will reschedule ScheduledBackupJob when the config is saved.


Concurrency and registration

  • Quartz is registered in API/DependencyInjection.cs via AddQuartzJobs(services, configuration).
  • Jobs that should not run in parallel are marked with [DisallowConcurrentExecution] (e.g. StoryCleanupJob, TempFileCleanupJob, PollExpirationJob, EmailCampaignJob, LicenseValidationJob).
  • Each job has a trigger with a cron schedule; the trigger identity follows the pattern {JobName}-trigger.