How to: Author and manage Dapr Jobs in the .NET SDK

Schedule jobs, receive HTTP or gRPC callbacks, and manage job registrations in .NET

Use Dapr.Jobs version 1.19.0 or later with a running Dapr sidecar and Scheduler. The complete management surface described here requires a runtime that supports the corresponding operations; use Dapr 1.19 for the 1.19 features. All client operations use version-aware stable/alpha API fallback, but that does not make newer management operations available on an older runtime.

dotnet add package Dapr.Jobs

Jobs are scoped to the scheduling application’s Dapr app ID. The client does not accept a different target app ID when scheduling a trigger. See the Jobs sample.

Register the client and callback handler

Register the client before building the host, and map one handler for dynamically named jobs:

using Dapr.Jobs;
using Dapr.Jobs.Extensions;
using Dapr.Jobs.Models;
using System.Text;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient();

await using var app = builder.Build();

app.MapDaprScheduledJobHandler(
    async (string jobName, ReadOnlyMemory<byte> payload, CancellationToken cancellationToken) =>
    {
        app.Logger.LogInformation("Received job {JobName}", jobName);
        var text = Encoding.UTF8.GetString(payload.Span);
        await HandleJobAsync(jobName, text, cancellationToken);
    },
    timeout: TimeSpan.FromSeconds(30));

await app.RunAsync();

HandleJobAsync represents your application logic. The callback receives the job name and original payload bytes; additional services can be resolved from request DI. Its cancellation token includes the configured handler timeout. Handlers must observe that token themselves; a timeout does not forcibly stop arbitrary code. Return a Task for asynchronous work so the callback waits for it before acknowledging the trigger.

For HTTP callbacks, the mapper registers POST /job/{jobName}. Start the application with a matching app port:

dapr run --app-id jobsapp --app-port 5000 -- dotnet run --urls http://localhost:5000

gRPC callbacks

AddDaprJobsClient and MapDaprScheduledJobHandler also register the gRPC callback path. Choose it with --app-protocol grpc. For plaintext gRPC, configure a dedicated HTTP/2 listener before builder.Build():

using Microsoft.AspNetCore.Server.Kestrel.Core;

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5000, listen => listen.Protocols = HttpProtocols.Http2);
});

An HTTP/1-only listener, or plaintext Http1AndHttp2 without protocol negotiation, cannot serve this gRPC callback. Use separate listeners if the same app must also accept HTTP/1 traffic.

Register the job

Resolve DaprJobsClient and use ScheduleJobAsync for every schedule type:

var jobsClient = app.Services.GetRequiredService<DaprJobsClient>();
var schedule = DaprJobSchedule.FromDateTime(DateTimeOffset.UtcNow.AddMinutes(5));

await jobsClient.ScheduleJobAsync(
    "send-receipt", schedule,
    payload: Encoding.UTF8.GetBytes("order-42"),
    overwrite: false,
    cancellationToken: cancellationToken);

Schedule only after the callback is ready, or use a future due time. Registering a job does not by itself start an application listener.

ArgumentMeaning
jobName, scheduleRequired name and DaprJobSchedule.
payloadOptional ReadOnlyMemory<byte> delivered unchanged to the callback.
startingFromOptional DateTimeOffset controlling the first due time.
repeatsOptional repeat count.
ttlOptional absolute DateTimeOffset expiry, later than startingFrom when both are supplied.
overwriteDefaults to false; opt in to replacing an existing same-name registration.
failurePolicyOptionsDrop or constant-interval retry policy for failed triggers.
cancellationTokenCancels waiting for the client operation; it is not the callback token.

One-time, interval, and predefined schedules

var oneTime = DaprJobSchedule.FromDateTime(DateTimeOffset.UtcNow.AddHours(1));
var interval = DaprJobSchedule.FromDuration(TimeSpan.FromMinutes(15));
var daily = DaprJobSchedule.Daily;

await jobsClient.ScheduleJobAsync("refresh-cache", interval, repeats: 5,
    cancellationToken: cancellationToken);

Predefined schedules also include Hourly, Midnight, Weekly, Monthly, and Yearly.

Cron-based job and time zone

Use FromExpression for a string expression, or FromCronExpression for a CronExpressionBuilder:

var schedule = DaprJobSchedule.FromExpression("CRON_TZ=America/New_York 0 9 * * *");
await jobsClient.ScheduleJobAsync("morning-report", schedule,
    cancellationToken: cancellationToken);

The builder also supports WithTimeZone(string) and WithTimeZone(TimeZoneInfo). DaprJobSchedule.TimeZone exposes an embedded CRON_TZ identifier. Use valid IANA time-zone IDs and account for daylight-saving changes when choosing calendar schedules.

JSON and string payload helpers

ScheduleJobWithPayloadAsync from Dapr.Jobs.Extensions accepts the same schedule:

await jobsClient.ScheduleJobWithPayloadAsync(
    "create-report", DaprJobSchedule.Daily,
    new { AccountId = "42", Format = "pdf" },
    cancellationToken: cancellationToken);

The object overload JSON-serializes with web defaults unless you pass jsonSerializerOptions. The string overload encodes UTF-8 text without JSON quoting. Deserialize callback bytes with the matching convention. There are no separate ScheduleCronJobAsync or ScheduleOneTimeJobWithPayloadAsync helpers in this release.

Configure trigger failure policy

await jobsClient.ScheduleJobAsync(
    "refresh-catalog", DaprJobSchedule.Hourly,
    failurePolicyOptions: new JobFailurePolicyConstantOptions(TimeSpan.FromSeconds(10))
    {
        MaxRetries = 5
    },
    cancellationToken: cancellationToken);

JobFailurePolicyConstantOptions retries at the specified interval; an unset MaxRetries allows indefinite retries. JobFailurePolicyDropOptions drops a failed trigger. An omitted policy leaves runtime defaults in effect. This policy is separate from the job’s recurring schedule and repeat count. Make callback side effects idempotent because triggers can be delivered more than once.

Get details of already-scheduled job

var details = await jobsClient.GetJobAsync("refresh-catalog", cancellationToken);

DaprJobDetails exposes Name, Schedule, DueTime, Ttl, RepeatCount, Payload, FailurePolicy, and Overwrite. Name can be null for manually constructed or older-runtime results. Inspect details.Schedule.ExpressionValue and schedule classification properties such as IsCronExpression and IsPointInTimeExpression. The response does not expose top-level Interval or CronExpression properties.

List all registered jobs

var registered = await jobsClient.ListJobsAsync(cancellationToken);
foreach (var job in registered)
{
    Console.WriteLine($"{job.Name}: {job.Schedule.ExpressionValue}");
}

The result is an IReadOnlyList<DaprJobDetails> scoped to the current app ID. An empty list normally means there are no jobs; also consider the release-specific cancellation behavior described below.

Delete a scheduled job

await jobsClient.DeleteJobAsync("refresh-catalog", cancellationToken);

Delete multiple jobs by name prefix

await jobsClient.DeleteJobsByPrefixAsync("report-", cancellationToken);

The prefix match is case-sensitive. A null, empty, or whitespace prefix deletes all jobs for this app ID. Validate a user-supplied prefix before calling this operation.

Purge all jobs

await jobsClient.PurgeAllJobsAsync(cancellationToken);

This explicitly requests deletion of every job associated with the current app ID, not every job in the cluster.

Errors and cancellation

Runtime failures are wrapped in DaprException; inspect InnerException. Invalid arguments can throw standard argument exceptions. In 1.19.0, several Jobs methods catch caller cancellation instead of throwing OperationCanceledException: writes/deletes can return without confirmation and listing can return empty. GetJobAsync reports a missing expected result as DaprException after cancellation. See DaprJobsClient usage for client configuration and payload conventions.

Analyzer

The Dapr.Jobs.Analyzers package ships diagnostic DAPR1501 (warning, category Usage). It fires when ScheduleJobAsync is called with a literal job name and no MapDaprScheduledJobHandler call exists in the same source file, with the message “Job invocations require the MapDaprScheduledJobHandler be set and configured for job name ‘{name}’ on IEndpointRouteBuilder”. Add the handler mapping, or suppress the diagnostic with #pragma warning disable DAPR1501 or an .editorconfig rule if the handler is intentionally mapped elsewhere.

Last modified October 22, 2026: Second pass - minor tweaks (2c8ac34b0)