Jobs
Jobs implement IJob and have a single handler. They support scheduling, retries, continuations, batches, named queues, and mutex.
Define a job
public class GenerateReport : IJob
{
public int ReportId { get; set; }
}
public class GenerateReportHandler : IJobHandler<GenerateReport>
{
public async Task HandleAsync(GenerateReport message, CancellationToken ct)
{
// Generate the report
}
}
Enqueue
await publisher.Enqueue(new GenerateReport { ReportId = 1 });
Schedule
await publisher.Schedule(new GenerateReport { ReportId = 1 }, DateTime.UtcNow.AddHours(1));
Retries
Declare retry policy on the handler:
[Retry(3)]
public class GenerateReportHandler : IJobHandler<GenerateReport>
{
// ...
}
Or on the job class:
[Retry(3, Delays = [15, 60, 300])]
public class GenerateReport : IJob
{
public int ReportId { get; set; }
}
Override per-enqueue via metadata:
await publisher.Enqueue(new GenerateReport { ReportId = 1 },
new JobParameters().Configure<IRetryMetadata>(m => m.MaxRetries = 5));
Configure global defaults:
services.AddJoblyRetry(o =>
{
o.MaxRetries = 3;
o.Delays = [15, 60, 300]; // seconds
o.JitterFactor = 0.2; // ±20% random jitter on each delay (default: 0, no jitter)
});
Priority: per-enqueue metadata > handler attribute > job attribute > global RetryOptions.
Failed jobs are retried automatically. Crash requeues (server died mid-execution) do not count against the retry limit.
JitterFactor is a multiplicative, global-only jitter applied to each computed delay: delay * (1 + JitterFactor * rand(-1, 1)). Clamped to [0, 1]. Useful when many jobs fail at the same time (e.g. downstream outage) to spread their retry attempts and avoid a thundering herd.
Named Queues
await publisher.Enqueue(new GenerateReport { ReportId = 1 }, queue: "critical");
Queues are processed in alphabetical order. A worker subscribes to specific queues:
options.Queues = new[] { "a-critical", "b-default", "c-low" };
Continuations
var parentId = await publisher.Enqueue(new ProcessPayment { OrderId = 1 });
await publisher.Enqueue(new SendReceipt { OrderId = 1 }, parentId); // Runs after parent completes
Batches
var batchPublisher = serviceProvider.GetRequiredService<IBatchPublisher>();
var jobs = orders.Select(o => new ProcessOrder { OrderId = o.Id }).ToList();
var batchId = await batchPublisher.StartNew(jobs);
// Continuation after batch completes
var followUps = new List<SendSummary> { new() };
await batchPublisher.ContinueBatchWith(followUps, batchId);
Batch continuation options control when continuations activate:
// Default: continuation only fires when ALL jobs succeed
await batchPublisher.StartNew(jobs, ContinuationOptions.OnlyOnSucceeded);
// Fire when all jobs finish, regardless of success/failure
await batchPublisher.StartNew(jobs, ContinuationOptions.OnAnyFinishedState);
With OnlyOnSucceeded (default): if any job in the batch fails, the batch itself transitions to Failed and continuations stay in Awaiting state indefinitely. You can requeue the failed jobs from the dashboard — if they succeed on retry and the batch completes, continuations will activate normally.
With OnAnyFinishedState: continuations fire as soon as all jobs reach a terminal state (Completed or Failed), regardless of outcome. Use this when the continuation needs to run even if some jobs failed (e.g., sending a summary report).
Recurring Jobs
Register a cron-based recurring job:
await recurringPublisher.AddOrUpdateRecurringJob(
new CleanupSessions(), name: "session-cleanup", cron: "0 * * * *");
This only registers the definition. The RecurringJobScheduler task creates jobs when the cron time arrives. See Recurring Jobs for full details.
Cancellation
Cancel a running job gracefully:
await jobCommandService.DeleteJob(jobId);
If the job is processing, this sets CancellationMode = Graceful instead of immediately changing state. The worker detects it and cancels the handler's CancellationToken. See Job Cancellation for the full flow.
Mutex
Only one job per mutex key can be processing at a time. Requires AddJoblyMutex():
await publisher.Enqueue(new ProcessPayment { CustomerId = 123 },
new JobParameters().WithMutex("payment:123"));
Or use the [Mutex] attribute on the job class for static keys:
[Mutex("payment-processing")]
public class ProcessPayment : IJob { ... }
See Mutex for details.