Recurring Jobs
Recurring jobs run on a cron schedule — database cleanup every night, report generation every Monday morning, health checks every minute. They implement IRecurringJob instead of IJob<T> and carry no payload.
Defining a recurring job
Create a class that implements IRecurringJob and decorate it with [JobConfig] to set the cron schedule:
[JobConfig(CronSchedule = "0 3 * * *", Queue = "maintenance", TimeoutSeconds = 300)]
public class CleanupExpiredSessions : IRecurringJob
{
private readonly AppDbContext _db;
public CleanupExpiredSessions(AppDbContext db) => _db = db;
public async Task ExecuteAsync(JobContext ctx)
{
var deleted = await _db.Sessions
.Where(s => s.ExpiresAt < DateTime.UtcNow)
.ExecuteDeleteAsync(ctx.CancellationToken);
ctx.Logger.LogInformation("Cleaned up {Count} expired sessions", deleted);
}
}
The CronSchedule property is only valid on IRecurringJob implementations. Everything else — Queue, MaxAttempts, TimeoutSeconds — works the same as regular jobs.
Cron expression format
Zeridion Flare uses 5-field cron expressions, parsed by the Cronos library:
┌───────────── minute (0–59)
│ ┌───────────── hour (0–23)
│ │ ┌───────────── day of month (1–31)
│ │ │ ┌───────────── month (1–12)
│ │ │ │ ┌───────────── day of week (0–6, Sunday = 0)
│ │ │ │ │
* * * * *
Common schedules
| Expression | Schedule |
|---|---|
* * * * * | Every minute |
*/5 * * * * | Every 5 minutes |
0 * * * * | Every hour, on the hour |
0 0 * * * | Daily at midnight |
0 3 * * * | Daily at 3:00 AM |
0 9 * * 1-5 | Weekdays at 9:00 AM |
0 0 * * 1 | Every Monday at midnight |
0 0 1 * * | First of each month at midnight |
0 0 1 1 * | January 1st at midnight |
Supported syntax
- Wildcards:
* - Ranges:
1-5 - Lists:
1,3,5 - Steps:
*/15 - Combined:
0 9-17 * * 1-5(every hour, 9 AM to 5 PM, weekdays)
6-field expressions (with seconds) are not supported. The minimum granularity is one minute.
Timezone handling
By default, cron expressions are evaluated in UTC. To schedule in a local timezone, set the timezone field when managing recurring jobs via the API:
PUT /v1/recurring/daily-report
Content-Type: application/json
{
"job_type": "GenerateDailyReport",
"cron_expression": "0 9 * * 1-5",
"timezone": "America/New_York",
"queue": "reports"
}
Timezone identifiers use the IANA Time Zone Database format (e.g., America/New_York, Europe/London, Asia/Tokyo). The API validates the timezone and returns 400 invalid_timezone if it is not recognized.
Timezone-aware scheduling correctly handles daylight saving time transitions.
Auto-registration from the SDK
When the SDK worker starts, it automatically discovers all IRecurringJob types with a CronSchedule attribute and registers them with the API via POST /v1/workers/register:
Each recurring job is registered with the ID pattern rjob_{jobType}. If the recurring schedule already exists, it is updated (upserted) with the latest configuration from the attribute.
This means you define recurring jobs in code and they are registered automatically — no manual API calls needed.
How scheduling works
The CronEvaluatorService (running in the Zeridion.CronScheduler host project) evaluates due recurring jobs every 60 seconds:
Each enqueued run is a normal Job entity that goes through the standard processing pipeline — poll, execute, ack, retry. The recurring schedule and the individual runs are independent.
Managing recurring jobs via API
Upsert a schedule
PUT /v1/recurring/{id}
Creates or updates a recurring job schedule. The id is user-defined (e.g., daily-cleanup, hourly-sync):
{
"job_type": "CleanupExpiredSessions",
"cron_expression": "0 3 * * *",
"timezone": "UTC",
"queue": "maintenance",
"max_attempts": 3,
"timeout_seconds": 300,
"enabled": true
}
List all schedules
GET /v1/recurring
Returns all recurring job schedules for the authenticated project.
Delete a schedule
DELETE /v1/recurring/{id}
Removes a recurring job schedule. Already-enqueued runs are not affected.
Enable/disable
Set "enabled": false in the upsert request to pause a recurring schedule without deleting it. The CronEvaluatorService skips disabled schedules during evaluation.
Missed run catch-up
If the CronScheduler is offline or delayed (e.g., during a deployment), it may miss one or more scheduled runs. When it comes back online:
- It enqueues one catch-up run per recurring job (not a backfill of all missed runs)
- This prevents a thundering herd of duplicate jobs on restart
- The
NextRunAtis recalculated from the current time after the catch-up
For critical schedules, monitor the LastRunAt field in GET /v1/recurring to verify that jobs are running on time.
Error handling
Recurring job runs follow the same retry strategy as regular jobs:
- If
ExecuteAsyncthrows, the run is retried with exponential backoff up toMaxAttempts - If all attempts are exhausted, the run moves to
DeadLetter - The recurring schedule is not affected — the next cron occurrence still fires on time
A failing run does not block future runs. Each scheduled execution is an independent job.
Unique job type constraint
Each project can only have one recurring schedule per job_type. If you try to create a second schedule with the same job_type under a different ID, the API returns 409 recurring_job_type_conflict.
Best practices
-
Keep recurring jobs lightweight — they run at regular intervals, so expensive operations accumulate. Offload heavy work to separate fire-and-forget jobs if needed.
-
Use dedicated queues — isolate recurring workloads with
[JobConfig(Queue = "maintenance")]so they don't compete with user-triggered jobs for worker slots. -
Design for idempotency — cron timing is approximate (evaluated every 60 seconds). Your job may run a few seconds early or late. If exact timing matters, check timestamps inside
ExecuteAsync. -
Monitor with metrics — use
GET /v1/metrics/summaryto track success rates for your recurring job types. A rising failure count signals a problem before it becomes critical.
See also
- IRecurringJob — interface reference
- JobConfigAttribute —
CronSchedule,Queue, and other defaults - Recurring API — PUT, GET, DELETE endpoints
- Queues and Concurrency — isolating recurring workloads