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.
| Job | Config key | Default schedule | Description |
|---|---|---|---|
| RefreshTokenCleanupJob | Quartz:RefreshTokenCleanupJob | 0 0 * * * ? (daily at midnight) | Deletes expired refresh tokens from the database. |
| UserSyncJob | Quartz:UserSyncJob | 0 */5 * * * ? (every 5 minutes) | Syncs user data from PostgreSQL to Neo4j (social graph). Skips when a Neo4j restore is in progress. |
| ElasticsearchSyncJob | Quartz:ElasticsearchSyncJob | 0 */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. |
| TempFileCleanupJob | Quartz:TempFileCleanupJob | 0 0 */6 * * ? (every 6 hours) | Deletes temporary files in the MinIO temp bucket older than 24 hours. |
| StoryCleanupJob | Quartz:StoryCleanupJob | 0 0 * * * ? (every hour) | Archives expired stories (e.g. older than 24 hours). Clears story-related caches after cleanup. |
| BirthdayNotificationJob | Quartz:BirthdayNotificationJob | 0 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. |
| EmailCampaignJob | Quartz:EmailCampaignJob | 0 * * ? * * (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. |
| PollExpirationJob | Quartz:PollExpirationJob | 0 */15 * * * ? (every 15 minutes) | Marks expired polls as expired; invalidates related cache. |
| LicenseValidationJob | Quartz:LicenseValidationJob | 0 0 0 ? * MON (every Monday at midnight) | Validates the license with the main server (CONSUMER mode only; skipped in MAIN mode). |
| ScheduledBackupJob | Quartz:ScheduledBackupJob or Backup:Schedule | 0 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. |
| SampleJob | Quartz:SampleJob | 0 * * ? * * (every minute) | Example/demo job (e.g. logs or simulates work). Can be disabled or removed in production. |
| BackgroundJobSample | Quartz: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:
| Job | Schedule source | Default | Description |
|---|---|---|---|
| BlogGenerationJob | Database (AI Agent settings) or config fallback AiAgentSettings:ScheduleCronExpression | 0 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.csviaAddQuartzJobs(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.
Related
- Workers — Kafka-based and other background workers
- Backend — API and worker project layout
- Email Campaigns — EmailCampaignJob behavior
- AI Blog Generation — BlogGenerationJob and worker
- Admin Panel → Backup — backup schedule configuration