Version v1.x
Job Model
Available in
Full job model explanation including interfaces, attributes, context, and registration behavior.
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.
public interface IDurableJob
{
Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default);
}
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>(...).
public interface IDurableJob<TArgs>
{
Task ExecuteAsync(TArgs args, JobContext context, CancellationToken cancellationToken = default);
}
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:
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;
}
}
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. Default3.RetryBehavior(RetryBehavior):FixedDelayorBackoff. DefaultFixedDelay.RetryInitialDelaySeconds(int): per-job initial retry delay. Default0meaning unset, which falls back to runtimeoptions.RetryDelay.
RecurringJobAttribute
Cron(string, constructor): required cron expression.TimeZone(string): IANA time zone ID. DefaultUTC.Enabled(bool): whether recurring schedule starts enabled. Defaulttrue.AllowConcurrentRuns(bool): whether overlapping recurring runs are allowed. Defaultfalse.
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(orruntime.enqueue(...)in Node.js)
var runId = await durableClient.EnqueueAsync<InvoiceSyncJob>(cancellationToken: cancellationToken);
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:
builder.Services.AddDurableJob<InvoiceSyncJob>(
name: "invoice-sync",
cronExpression: "*/5 * * * *",
timeZone: "UTC",
maxAttempts: 5);
runtime.registerRecurring("invoice-sync", "*/5 * * * *", "UTC", async () => {
console.log("invoice-sync run");
}, {
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);
});
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, typedregisterJob<TArgs>(...)in Node.js). - Keep job classes focused on orchestration and call domain services for business logic.