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

# Job Model

In DurableStack, a job is registered work executed by the runtime.

In .NET, that usually means a class implementing a job interface.

In Node.js, that means registering a handler function with `runtime.registerJob(...)` or `runtime.registerRecurring(...)`.

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

## Core job contracts

In .NET, use `IDurableJob` when a job has no payload.

In Node.js, register a handler with the same logical job name and no payload type.

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

```typescript
runtime.registerJob("invoice-sync", async (context) => {
  console.log(context.runId);
});
```
:::

In .NET, use `IDurableJob<TArgs>` when a job needs typed input.

In Node.js, model typed input with a TypeScript type/interface and pass it to `runtime.registerJob<TArgs>(...)`.

:::code-group
```csharp
public interface IDurableJob<TArgs>
{
    Task ExecuteAsync(TArgs args, JobContext context, CancellationToken cancellationToken = default);
}
```

```typescript
type SendWelcomeEmailArgs = {
  email: string;
};

runtime.registerJob<SendWelcomeEmailArgs>("send-welcome-email", async (args, context) => {
  console.log(args.email, context.runId);
});
```
:::

## What `JobContext` gives you

`JobContext` includes these core concepts (property names differ by runtime):

- `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` (.NET): scoped service provider for resolving dependencies during execution.

## Recommended model: runtime-native metadata

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

In Node.js, jobs are registered explicitly at runtime startup.

Use attributes in .NET and registration options in Node.js for execution and schedule metadata:

:::code-group
```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;
    }
}
```

```typescript
runtime.registerRecurring(
  "invoice-sync",
  "*/5 * * * *",
  "UTC",
  async () => {
    console.log("invoice-sync run");
  },
  {
    maxAttempts: 5,
    retryBehavior: "Backoff",
    retryInitialDelaySeconds: 10,
    enabled: true,
    allowConcurrentRuns: false
  }
);
```
:::

## .NET 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`.

## Node.js registration options (equivalent concepts)

- `maxAttempts`: total attempts including initial execution.
- `retryBehavior`: `"FixedDelay"` or `"Backoff"`.
- `retryInitialDelaySeconds`: per-job initial retry delay.
- `enabled` (recurring): whether recurring schedule starts enabled.
- `allowConcurrentRuns` (recurring): whether overlapping runs are allowed.

## One job, two execution paths

A .NET job with only `DurableJob` is enqueue-only.

A .NET job with both `DurableJob` and `RecurringJob` supports:

- automatic recurring materialization from cron
- manual enqueue/run-now using `IDurableStackClient` (or `runtime.enqueue(...)` in Node.js)

:::code-group
```csharp
var runId = await durableClient.EnqueueAsync<InvoiceSyncJob>(cancellationToken: cancellationToken);
```

```typescript
const runId = await runtime.enqueue("invoice-sync");
```
:::

## Runtime registration behavior

During startup, DurableStack builds runtime job registrations.

In .NET, this comes from discovered/registered job classes.

In Node.js, this comes from explicit `runtime.registerJob(...)` / `runtime.registerRecurring(...)` calls.

Key registration rules:

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

## Activation model and dependency resolution (.NET)

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

In .NET, you can explicitly register jobs instead of auto-discovery:

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

```typescript
runtime.registerRecurring("invoice-sync", "*/5 * * * *", "UTC", async () => {
  console.log("invoice-sync run");
}, {
  maxAttempts: 5
});
```
:::

Or with options:

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

```typescript
runtime.registerRecurring("invoice-sync", "*/5 * * * *", "UTC", async () => {
  console.log("invoice-sync run");
}, {
  maxAttempts: 5,
  retryBehavior: "Backoff",
  retryInitialDelaySeconds: 10,
  allowConcurrentRuns: false
});
```
:::

## Practical guidance

- Keep job names stable once production data exists.
- Keep handlers idempotent so retries are safe.
- Use typed payload contracts when payload shape matters (`IDurableJob<TArgs>` in .NET, typed `registerJob<TArgs>(...)` in Node.js).
- Keep job classes focused on orchestration and call domain services for business logic.
