---
title: Job Model
description: Full job model explanation including interfaces, attributes, context, and registration behavior.
order: 21
---

# 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:

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

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

```csharp
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.

## Recommended model: attribute-based job metadata

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

Use attributes for execution and schedule metadata:

```csharp
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`

```csharp
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:

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

Or with options:

```csharp
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.
