Scheduling
NexJob's IScheduler interface is your single entry point for dispatching background work. Inject it into any service, controller, or minimal API handler, then choose whether to run a job immediately, after a relative delay, or at a precise UTC timestamp. Every method returns the JobId of the created record, which you can use for continuations, tag queries, or status checks.
Immediate Enqueue¶
Call EnqueueAsync to place a job in the queue right now. The dispatcher picks it up on the next wake-up cycle.
// No input
await scheduler.EnqueueAsync<SendEmailJob>(cancellationToken: ct);
// With typed input
await scheduler.EnqueueAsync<SendEmailJob, SendEmailInput>(
new SendEmailInput("user@example.com", "Welcome!"),
cancellationToken: ct);
The job transitions to Enqueued state immediately and is eligible for processing straight away.
Scheduled Execution¶
Delay with TimeSpan¶
Use ScheduleAsync when you want the job to become eligible after a relative duration from now:
// Run in 30 minutes
await scheduler.ScheduleAsync<SendReminderJob>(
delay: TimeSpan.FromMinutes(30),
cancellationToken: ct);
// With input
await scheduler.ScheduleAsync<SendReminderJob, ReminderInput>(
input: new ReminderInput(userId),
delay: TimeSpan.FromMinutes(30),
cancellationToken: ct);
Schedule at a Specific Time¶
Use ScheduleAtAsync when you need the job to run at a known UTC instant, such as the start of a business day:
// Run tomorrow at 8 AM UTC
var runAt = new DateTimeOffset(
DateTimeOffset.UtcNow.Date.AddDays(1).AddHours(8),
TimeSpan.Zero);
await scheduler.ScheduleAtAsync<GenerateReportJob>(
runAt: runAt,
cancellationToken: ct);
// With input
await scheduler.ScheduleAtAsync<GenerateReportJob, ReportInput>(
input: new ReportInput("monthly"),
runAt: runAt,
cancellationToken: ct);
Scheduled jobs are stored with status Scheduled and a ScheduledAt timestamp. The dispatcher will not pick them up until UtcNow >= ScheduledAt.
Priority¶
Every job has a priority level that controls the order in which the dispatcher fetches work. The default is Normal. Within the same priority, jobs are processed FIFO.
await scheduler.EnqueueAsync<CriticalAlertJob>(
priority: JobPriority.Critical,
cancellationToken: ct);
await scheduler.EnqueueAsync<BackgroundSyncJob>(
priority: JobPriority.Low,
cancellationToken: ct);
| Priority | Value | Typical Use Case |
|---|---|---|
Critical |
1 | Payment failures, security alerts |
High |
2 | User-facing operations needing fast response |
Normal |
3 | Default — emails, notifications, webhooks |
Low |
4 | Cleanup, archival, analytics, batch imports |
The dispatcher fetches jobs in ascending value order, so Critical (1) is always processed before Low (4).
Queue Routing¶
Route jobs to dedicated processing pipelines by specifying a queue name. This lets you isolate workloads — for example, keeping heavy computation off the same workers that serve user-facing notifications.
await scheduler.EnqueueAsync<HeavyComputationJob>(
queue: "compute",
cancellationToken: ct);
await scheduler.EnqueueAsync<SendEmailJob>(
queue: "notifications",
cancellationToken: ct);
Configure which queues each dispatcher instance monitors:
builder.Services.AddNexJob(options =>
{
options.Queues = new[] { "default", "notifications", "compute" };
});
A dispatcher only processes the queues listed in its Queues configuration. Deploy separate worker instances with different queue lists to achieve workload isolation. See Queues for fetch order, per-queue settings and runtime control.
Deadlines¶
Use deadlineAfter to mark a job as expired if it has not started executing within the given window. This is useful for time-sensitive notifications that have no value once the window has passed.
// Expire this job if it hasn't started within 10 minutes
await scheduler.EnqueueAsync<SendPromotionalEmailJob>(
deadlineAfter: TimeSpan.FromMinutes(10),
cancellationToken: ct);
Warning
A job past its deadline is marked Expired and never executes — it does not retry and no dead-letter handler is called. Only use deadlineAfter when skipping the work is an acceptable outcome.
deadlineAfter is supported only on EnqueueAsync. Scheduled jobs (ScheduleAsync, ScheduleAtAsync) do not accept a deadline — use the runAt timestamp as your natural deadline instead.
Idempotency Keys and Tags¶
Attach an idempotencyKey to prevent the same logical job from being enqueued twice, and use tags to attach searchable metadata:
await scheduler.EnqueueAsync<ProcessOrderJob, ProcessOrderInput>(
input: new ProcessOrderInput(orderId),
idempotencyKey: $"order-{orderId}",
tags: new[] { "order", orderId.ToString() },
cancellationToken: ct);
// Retrieve by tag later
var orderJobs = await scheduler.GetJobsByTagAsync(orderId.ToString(), ct);
See Idempotency for the full deduplication rules and DuplicatePolicy options.
IScheduler Method Cheat Sheet¶
The table below lists every method on IScheduler and its key parameters:
| Method | Purpose | Key Parameters |
|---|---|---|
EnqueueAsync<TJob>(...) |
Run immediately, no input | queue, priority, idempotencyKey, duplicatePolicy, tags, deadlineAfter, ct |
EnqueueAsync<TJob, TInput>(input, ...) |
Run immediately with typed input | input, queue, priority, idempotencyKey, duplicatePolicy, tags, deadlineAfter, ct |
ScheduleAsync<TJob>(delay, ...) |
Run after a relative delay | delay (TimeSpan), queue, idempotencyKey, ct |
ScheduleAsync<TJob, TInput>(input, delay, ...) |
Run with input after a delay | input, delay (TimeSpan), queue, idempotencyKey, ct |
EnqueueAsync<TJob>(maxAttempts, ...) / EnqueueAsync<TJob, TInput>(input, maxAttempts, ...) |
Run immediately with its own attempt limit | maxAttempts (at least 1), then the same options as above |
ScheduleAsync<TJob>(delay, maxAttempts, ...) / ScheduleAsync<TJob, TInput>(input, delay, maxAttempts, ...) |
Run after a delay with its own attempt limit | maxAttempts (at least 1), delay, queue, idempotencyKey, ct |
ScheduleAtAsync<TJob>(runAt, ...) |
Run at a fixed UTC time | runAt (DateTimeOffset), queue, idempotencyKey, ct |
ScheduleAtAsync<TJob, TInput>(input, runAt, ...) |
Run with input at a fixed UTC time | input, runAt (DateTimeOffset), queue, idempotencyKey, ct |
RecurringAsync<TJob>(id, cron, ...) |
Create or update a cron schedule | recurringJobId, cron, timeZone, queue, concurrencyPolicy, ct |
RecurringAsync<TJob, TInput>(id, input, cron, ...) |
Cron schedule with typed input | recurringJobId, input, cron, timeZone, queue, concurrencyPolicy, ct |
RemoveRecurringAsync(id, ct) |
Delete a recurring schedule | recurringJobId, ct |
ContinueWithAsync<TJob>(parentJobId, ...) |
Run after parent job succeeds | parentJobId, queue, ct |
ContinueWithAsync<TJob, TInput>(parentJobId, input, ...) |
Run with input after parent succeeds | parentJobId, input, queue, ct |
GetJobsByTagAsync(tag, ct) |
Find jobs by tag | tag, ct |
Parameter Quick Reference¶
-
queue
Queue name. When
null, the default queue of the application ({prefix}.default). -
priority
Critical(1),High(2),Normal(3, default),Low(4). -
idempotencyKey
Unique deduplication key. Returns the existing
JobIdif the job is still active. -
duplicatePolicy
AllowAfterFailed(default),RejectIfFailed,RejectAlways. -
deadlineAfter
Max time before the job expires if not picked up.
EnqueueAsynconly. -
tags
Searchable string labels for dashboard filtering and
GetJobsByTagAsync.