Skip to content

Jobs

Call AddJob with a name and a delegate. Parameters are resolved from DI, job arguments, or special types like JobContext and CancellationToken:

// Simple job with parameters
app.AddJob("Add", (int a, int b) => a + b);
// Async job with DI services
app.AddJob("Import", async (ILogger<Program> logger, CancellationToken ct) =>
{
logger.LogInformation("Starting import");
await Task.Delay(5000, ct);
});
// Progress reporting (0.0 to 1.0)
app.AddJob("DataImport", async (JobContext ctx, CancellationToken ct) =>
{
for (var i = 1; i <= 10; i++)
{
await ctx.ReportProgressAsync(i / 10.0);
await Task.Delay(1000, ct);
}
});

Default parameter values work too. If count isn’t provided when triggering, it defaults to 100:

app.AddJob("Process", async (JobContext ctx, CancellationToken ct, int count = 100) =>
{
for (var i = 0; i < count; i++)
{
await ctx.ReportProgressAsync((double)i / count);
await Task.Delay(500, ct);
}
});

Parameters are resolved in this order:

  1. Special typesJobContext and CancellationToken are injected directly.
  2. DI servicesIJobClient, ILogger<T>, and any registered services are resolved from the scoped service provider.
  3. Job arguments are deserialized from the JSON arguments passed when the job was triggered.

If a parameter has a default value and isn’t provided in the arguments, the default is used.

Chain configuration methods after AddJob:

app.AddJob("Cleanup", async () => { /* ... */ })
.WithCron("0 * * * *", "America/Chicago")
.WithDescription("Hourly cleanup")
.WithTags("maintenance")
.WithTimeout(TimeSpan.FromMinutes(5))
.WithMaxConcurrency(1)
.WithPriority(5)
.WithQueue("maintenance")
.WithRateLimit("api-calls")
.WithRetry(3);
MethodDescription
WithCron(expr, tz?)Run on a cron schedule, optionally in a specific timezone
WithDescription(text)Description shown in the dashboard
WithTags(tags)Tags for filtering in the dashboard
WithTimeout(duration)Cancel the job if it runs longer than this
WithMaxConcurrency(n)Max simultaneous runs of this job across the cluster
WithPriority(n)Higher priority runs are claimed first (default 0)
WithQueue(name)Assign to a named queue
WithRateLimit(name)Apply a named rate limit
WithRetry(n)Max number of attempts
WithRetry(configure)Fine-grained retry policy (backoff, delays, jitter)
WithMisfirePolicy(policy)What to do when scheduled fires are missed
Continuous()Auto-restart when the job completes, fails, or is cancelled

Hook into job lifecycle events per job or globally.

app.AddJob("Order", async (int orderId) => { /* ... */ })
.WithRetry(3)
.OnSuccess((JobContext ctx, ILogger<Program> logger) =>
{
logger.LogInformation("Completed on attempt {Attempt}", ctx.Attempt);
})
.OnRetry((JobContext ctx, ILogger<Program> logger) =>
{
logger.LogWarning("Retrying attempt {Attempt} for {JobName}", ctx.Attempt, ctx.JobName);
})
.OnDeadLetter((JobContext ctx) =>
{
// All retries exhausted
});
  • OnSuccess fires when a job completes successfully.
  • OnRetry fires when Surefire schedules another attempt after a failure.
  • OnDeadLetter fires when all retries are exhausted.

Global callbacks apply to every job:

builder.Services.AddSurefire(options =>
{
options.OnDeadLetter((JobContext ctx, ILogger<Program> logger) =>
{
logger.LogError(ctx.Exception, "Job {Name} reached dead letter", ctx.JobName);
});
});