Skip to content

Creating jobs

Declare payload and payloadless jobs, choose stable names, and configure execution and retry policy.

2 min read

A job is a non-nested partial class carrying both [Handler] and [Job]. It declares exactly one private instance HandleAsync method returning ValueTask; its request comes first and CancellationToken last. Dependencies between those two parameters are resolved through the normal Immediate.Handlers pipeline.

[Handler, Job(Name = "send-welcome-email", MaxAttempts = 5, Timeout = "00:02:00")]
public sealed partial class SendWelcomeEmail(IEmailSender sender)
{
	public sealed record Payload(Guid UserId, string Template);

	private ValueTask HandleAsync(Payload payload, CancellationToken cancellationToken) =>
		new(sender.SendAsync(payload.UserId, payload.Template, cancellationToken));
}

Payload and payloadless jobs

A payload is persisted as JSON. It must be source-generatable by System.Text.Json: use public readable properties and supported concrete types. One-dimensional arrays, List<T> and Dictionary<TKey, TValue> receive generated collection metadata; other IEnumerable<T> shapes such as HashSet<T>, multidimensional arrays and dictionaries with unsupported key types are rejected. Interfaces, abstract types, open generics, pointers, delegates, ref-like types and unsupported System types are also rejected.

Prefer records or classes with a public constructor whose parameters match their public members. Constructor availability is not currently an analyzer error, so a type that receives metadata but cannot be constructed by System.Text.Json can still fail when a worker deserializes it. Generated metadata is used for both serialization and Native AOT; no reflection fallback is needed.

A payloadless job receives EmptyJobRequest:

[Handler, Job(Cron = "0 */5 * * * *", TimeZone = "Europe/Vienna")]
public sealed partial class CleanupSessionsJob(AppDbContext db)
{
	private ValueTask HandleAsync(EmptyJobRequest request, CancellationToken cancellationToken) =>
		new(db.DeleteExpiredSessions(cancellationToken));
}

Only payloadless jobs can have a cron schedule. Payloadless jobs without Cron instead receive a dynamic recurring scheduler.

Stable persisted names

Name is persisted and connects stored rows to the generated definition. When omitted it is derived from the class name (SendWelcomeEmailJob becomes send-welcome-email). Set an explicit name before production and do not casually rename it: old rows retain the old value and will fail when no matching definition is registered. Names must contain a letter or digit and be unique in the assembly.

Execution policy

[Job(
	Name = "send-welcome-email",
	MaxAttempts = 5,
	Timeout = "00:02:00",
	MaxConcurrency = 8,
	Backoff = BackoffStrategy.ExponentialJitter,
	BackoffBase = "00:00:05"
)]
SettingDefaultMeaning
MaxAttempts3Total attempts including the first; must be at least one.
TimeoutnullPer-attempt timeout as an invariant TimeSpan; null means none.
MaxConcurrency0Maximum simultaneous executions per node; zero is unbounded.
BackoffExponentialJitterFixed, Exponential, or bounded ExponentialJitter.
BackoffBase00:00:05Positive base retry delay.
OverlapPolicySkipRecurring-only: Skip, Queue, or Concurrent.

Queue concurrency, job concurrency and node-wide MaxParallelJobs all apply. The effective capacity is whichever limit is reached first.