Quartz.NETQuartz.NET
Home
Features
Blog
Discussions
NuGet
GitHub
Home
Features
Blog
Discussions
NuGet
GitHub
  • Getting Started

    • Overview
    • Quartz 4 Quick Start
    • Tutorial
      • Using Quartz
      • Jobs And Triggers
      • More About Jobs & JobDetails
      • Job Data
      • More About Triggers
      • Querying Jobs and Triggers
      • Simple Triggers
      • Cron Triggers
      • RecurrenceTrigger
      • Time and TimeProvider
      • Trigger and Job Listeners
      • Scheduler Listeners
      • Job Execution Middleware
      • Job Stores
      • Configuration, Resource Usage and Building a Scheduler
      • Building a Scheduler Without a Host
      • Clustering
      • Execution Groups
      • Node Affinity (Preferred Node)
      • Testing
      • Compile-Time Checks
      • Declaring Jobs with Attributes
      • Delegate Jobs
    • Configuration Reference
    • JSON Configuration
    • Cron Expression Reference
    • Multi-Tenancy
    • Comparison
    • Frequently Asked Questions
    • Best Practices
    • Before You Go Live
    • Operating a Cluster
    • Log Events
    • Tenancy Patterns
    • Database Schema
    • Database Schema Changes
    • Migration Guide
    • Troubleshooting
    • API Documentation
  • How To's
    • One-Off Job
    • Rescheduling Jobs
    • Retrying Failed Jobs
    • Job Continuations
    • Progress and Execution Logs
    • Multiple Triggers
    • Job Template
    • Running Quartz under Aspire
    • Quartz.NET with Wolverine
    • Coming from Hangfire
    • Coming from TickerQ
    • Embedding Quartz in a Library
    • Running under an External Leader Election
    • Publishing Trimmed and Native AOT
    • Extending Quartz: what is open, what is closed, and how to ask
    • A Job Store of Your Own
    • A Driver Delegate for a New Database
    • Persisting a Custom Trigger Type
    • A Lock Handler of Your Own
  • Packages

    • Quartz Core Additions

      • Jobs
      • Serialization (System.Text.Json)
      • JSON Serialization
      • Plugins
    • Integrations

      • Aspire Integration
      • ASP.NET Core Integration
      • HTTP API
      • HTTP Client
      • Dashboard
      • Hosted Services Integration
      • Microsoft DI Integration
      • Multiple Schedulers with Microsoft DI
      • Observability
      • Redis Lock Handler
      • TimeZoneConverter Integration
    • 3rd Party Plugins for Quartz
  • Quartz 3.x

    • Getting Started

      • Quartz 3 Quick Start
      • Tutorial
        • Using Quartz
        • Library Overview
        • Jobs And Triggers
        • More About Jobs
        • More About Triggers
        • Execution Groups
        • Node Affinity (Preferred Node)
        • Simple Triggers
        • Cron Triggers
        • RecurrenceTrigger
        • Trigger and Job Listeners
        • Scheduler Listeners
        • Job Stores
        • Tuning the Scheduler
        • Configuration, Resource Usage and SchedulerFactory
        • Advanced (Enterprise) Features
      • Configuration Reference
      • JSON Configuration
      • Multi-Tenancy
      • Frequently Asked Questions
      • Best Practices
      • Tenancy Patterns
      • Troubleshooting
      • API Documentation
      • Database Schema
      • Database Schema Changes
      • Migration Guide
      • Miscellaneous Features
    • How To's

      • One-Off Job
      • Multiple Triggers
      • Job Template
      • Using the CronTrigger
      • Rescheduling Jobs
    • Packages

      • Quartz Core Additions

        • Dashboard
        • Jobs
        • Serialization (System.Text.Json)
        • Serialization (Newtonsoft Json.NET)
        • Plugins
      • Integrations

        • ASP.NET Core Integration
        • Hosted Services Integration
        • Microsoft DI Integration
        • Multiple Schedulers with Microsoft DI
        • OpenTelemetry Integration
        • OpenTracing Integration
        • Redis Lock Handler
        • TimeZoneConverter Integration
      • 3rd Party Plugins for Quartz
  • Old Releases

    • Quartz 2.x
      • Quartz 2 Quick Start
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
        • CronTrigger Tutorial
      • Configuration Reference
      • Migration Guide
      • API Documentation
    • Quartz 1.x
      • Tutorial
        • Lesson 1: Using Quartz
        • Lesson 2: Jobs And Triggers
        • Lesson 3: More About Jobs & JobDetails
        • Lesson 4: More About Triggers
        • Lesson 5: SimpleTrigger
        • Lesson 6: CronTrigger
        • Lesson 7: TriggerListeners and JobListeners
        • Lesson 8: SchedulerListeners
        • Lesson 9: JobStores
        • Lesson 10: Configuration, Resource Usage and SchedulerFactory
        • Lesson 11: Advanced (Enterprise) Features
        • Lesson 12: Miscellaneous Features of Quartz
      • API Documentation
  • License

Version compatibility

This documentation relates to Quartz version 4.3 and later.

A delegate job is a lambda whose parameters are what it needs. ScheduleJob adds one with its trigger, and AddJob adds one on its own.

services.AddQuartz(q =>
{
    q.ScheduleJob(
        "session-cleanup",
        static async (ISessionStore sessions, ILogger<SessionCleanup> log, CancellationToken cancellationToken) =>
        {
            int purged = await sessions.PurgeExpired(cancellationToken);
            log.LogInformation("Purged {Count} expired sessions", purged);
        },
        trigger => trigger.WithCronSchedule("0 0 * * * ?"));
});

services.AddQuartzHostedService();

What each parameter is handed

Parameter typeHanded
IJobExecutionContextthe firing
CancellationTokenthe firing's token, the same one context.CancellationToken carries
IServiceProviderthe firing's DI scope
any other typeGetRequiredService from that scope
  • Write every parameter's type. A lambda without them has no delegate type and does not compile.
  • Return Task, ValueTask or nothing. AddJob and ScheduleJob throw ArgumentException for any other return type, async void, a ref, out, in, pointer or ref struct parameter, and a combined delegate.
  • async () => await repo.Count() returns Task<int>, and is refused. A job has no result. Use a block body, or set IJobExecutionContext.Result.
  • A missing service fails the firing, not the registration: the run shell reports a JobExecutionException whose inner exception is the container's.
  • A scheduler's own parts fail validation at startup: IScheduler, ISchedulerFactory, a scheduler's options. Take IJobExecutionContext and read context.Scheduler. It is the rule for a registered job's constructor.
  • Job data is context.MergedJobDataMap. It is not applied to properties, since a lambda has none.

The parameters are read once, when the job is added, and the handler runs through reflection. It works trimmed and under native AOT: the repository's trimming canary runs a delegate job in its native binary. #3882 replaces the reflection with generated code.

Adding one

CallThe jobIts trigger
ScheduleJob(name, handler, trigger)takes its trigger's identity; not durablenamed name unless trigger renames it
AddJob(name, handler, configure)name in the default group, unless configure sets an identity; durablenone: add one with AddTrigger, or fire it with TriggerJob

Each has a twin whose callback also takes the IServiceProvider, for configuration read from services.

services.AddQuartz(q =>
{
    // Durable, and fired only by a trigger of its own or by TriggerJob.
    q.AddJob("send-digest", static (IEmailSender email, CancellationToken cancellationToken) =>
        email.SendDigest(cancellationToken));

    q.AddTrigger(trigger => trigger
        .ForJob("send-digest")
        .WithCronSchedule("0 0 7 ? * MON-FRI"));
});

A one-off firing is the named job fired with data of its own:

// A one-off firing of the named job, carrying data of its own.
await scheduler.TriggerJob(
    new JobKey("send-digest"),
    new JobDataMap { ["recipient"] = "[email protected]" },
    cancellationToken);
services.AddQuartz(q =>
{
    q.AddJob("send-digest", static (IJobExecutionContext context, ILogger<SessionCleanup> log) =>
    {
        string? recipient = context.MergedJobDataMap.GetString("recipient");
        log.LogInformation("Digest for {Recipient}, fired by {Trigger}", recipient, context.Trigger.Key);
    });
});

Persistence and clusters

  • Every delegate job is stored as Quartz.Impl.DelegateJob, Quartz. The name resolves on every node, so the job persists and clusters like any other. Its Description defaults to Delegate job '<name>'.
  • The job key is the identity. The handler is code and is not stored. A firing finds it by the job's key, on the scheduler that fires it.
  • Register the job on every node that runs the scheduler. A node without it fails the firing with a JobExecutionException naming the key and the scheduler, so the trigger's retry policy and SchedulerError apply.
  • Two handlers under one key on one scheduler are refused when the scheduler starts.
  • Named schedulers keep their own handlers. The same key on two schedulers is two jobs.
  • An IsJobTypeAllowed allow-list on the HTTP API or the dashboard must allow Quartz.Impl.DelegateJob, Quartz for a caller to add a delegate job by type name. Such a job runs only if a handler is registered under its key.

What composes

FeatureWith a delegate job
[DisallowConcurrentExecution].DisallowConcurrentExecution() in AddJob's configure; both stores enforce it
[PersistJobDataAfterExecution].PersistJobDataAfterExecution() in AddJob's configure
[JobTimeout]AddJobTimeout(defaultTimeout); there is no attribute to put on a lambda
Retry policy, execution group, preferred node, calendaron the trigger, as for any job
Middleware, listeners, execution history, metricsunchanged; the job type they report is DelegateJob
ConfigureJobScoperuns before the handler's services are resolved
services.AddQuartz(q =>
{
    // DelegateJob carries no [JobTimeout], so the scheduler-wide default bounds it.
    q.AddJobTimeout(TimeSpan.FromMinutes(5));

    q.AddJob(
        "reindex",
        static async (ISessionStore sessions, CancellationToken cancellationToken) =>
        {
            await sessions.PurgeExpired(cancellationToken);
        },
        job => job
            .WithIdentity("reindex", "maintenance")
            .WithDescription("Rebuilds the session index")
            // The attribute's builder form: every delegate job shares one type.
            .DisallowConcurrentExecution());

    q.AddTrigger(trigger => trigger
        .ForJob("reindex", "maintenance")
        .WithCronSchedule("0 0/15 * * * ?")
        .WithRetryPolicy(RetryPolicy.Exponential(3, TimeSpan.FromSeconds(30))));
});

When to write an IJob class instead

  • The job needs a typed input: IJob<TInput> and ScheduleJob<TJob, TInput> take a class.
  • It needs a [JobTimeout] of its own, rather than the scheduler-wide default.
  • It should be tested, reused or found by name in the code base.
  • Job data should arrive as properties.
  • Not every node that runs the scheduler registers the same jobs. A class resolves by its type name; a delegate job needs its handler registered.

Related

  • Using Quartz: AddJob<T>, AddTrigger<T> and ScheduleJob<T>
  • Declaring Jobs with Attributes: a class declared with [QuartzJob] and [CronTrigger]
  • One-Off Job: many short-lived firings of one job
Help us by improving this page!
Last Updated: 9/26/26, 7:42 PM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
Declaring Jobs with Attributes