Version v1.0 · dotnet

Job Model

Full job model explanation including interfaces, attributes, context, and registration behavior.

Job Model

In DurableStack, a job is a class that implements a supported job contract and is registered in the runtime job registry.

Each execution of that job is a run with its own lifecycle, attempt count, and status transitions.

Core job contracts

Use IDurableJob when a job has no payload:

public interface IDurableJob
{
    Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default);
}

Use IDurableJob<TArgs> when a job needs typed input:

public interface IDurableJob<TArgs>
{
    Task ExecuteAsync(TArgs args, JobContext context, CancellationToken cancellationToken = default);
}

What JobContext gives you

JobContext includes:

  • RunId: unique identifier for the specific run instance.
  • JobName: logical job name from registration.
  • Attempt: current attempt number for this run.
  • ScheduledForUtc: the intended schedule/enqueue time for this run.
  • Services: scoped service provider for resolving dependencies during execution.

DurableStack discovers public job classes automatically when options.JobRegistration.AutoDiscoverJobsFromAssembly is true (default).

Use attributes for execution and schedule metadata:

using DurableStack.Core;
using DurableStack.Core.Abstractions;
using DurableStack.Core.Models;
using DurableStack.Hosting.DependencyInjection;

[DurableJob(
    Name = "invoice-sync",
    MaxAttempts = 5,
    RetryBehavior = RetryBehavior.Backoff,
    RetryInitialDelaySeconds = 10)]
[RecurringJob(
    "*/5 * * * *",
    TimeZone = "UTC",
    Enabled = true,
    AllowConcurrentRuns = false)]
public sealed class InvoiceSyncJob : IDurableJob
{
    public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }
}

Full attribute options and defaults

DurableJobAttribute

  • Name (string?): optional stable job name. Default is class name.
  • MaxAttempts (int): total attempts including initial attempt. Default 3.
  • RetryBehavior (RetryBehavior): FixedDelay or Backoff. Default FixedDelay.
  • RetryInitialDelaySeconds (int): per-job initial retry delay. Default 0 meaning unset, which falls back to runtime options.RetryDelay.

RecurringJobAttribute

  • Cron (string, constructor): required cron expression.
  • TimeZone (string): IANA time zone ID. Default UTC.
  • Enabled (bool): whether recurring schedule starts enabled. Default true.
  • AllowConcurrentRuns (bool): whether overlapping recurring runs are allowed. Default false.

One job, two execution paths

A job with only DurableJob is enqueue-only.

A job with both DurableJob and RecurringJob supports:

  • automatic recurring materialization from cron
  • manual enqueue/run-now using IDurableStackClient
var runId = await durableClient.EnqueueAsync<InvoiceSyncJob>(cancellationToken: cancellationToken);

Runtime registration behavior

During startup, DurableStack builds DurableJobRegistration entries from discovered job classes.

Key registration rules:

  • Duplicate job names are rejected.
  • Duplicate job types are rejected.
  • Invalid MaxAttempts (<= 0) fails startup.
  • Invalid cron or invalid time zone fails startup.

Activation model and dependency resolution

Jobs are resolved from DI using one of two activation modes:

  • ScopedPerExecution (default): creates a fresh scope per run.
  • RootProvider: resolves from root provider (scoped dependencies are not supported).

In most cases, keep ScopedPerExecution.

Explicit registration for advanced scenarios

You can explicitly register jobs instead of auto-discovery:

builder.Services.AddDurableJob<InvoiceSyncJob>(
    name: "invoice-sync",
    cronExpression: "*/5 * * * *",
    timeZone: "UTC",
    maxAttempts: 5);

Or with options:

builder.Services.AddDurableJob<InvoiceSyncJob>("invoice-sync", options =>
{
    options.WithMaxAttempts(5)
           .WithRetryBehavior(RetryBehavior.Backoff)
           .WithRetryInitialDelaySeconds(10)
           .RunOnCron("*/5 * * * *", timeZone: "UTC")
           .WithAllowConcurrentRuns(false);
});

Practical guidance

  • Keep job names stable once production data exists.
  • Keep handlers idempotent so retries are safe.
  • Use typed jobs (IDurableJob<TArgs>) when payload shape matters.
  • Keep job classes focused on orchestration and call domain services for business logic.