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
    • Backfill
    • Retrying Failed Jobs
    • Pausing with a Reason
    • Job Continuations
    • Overlap Policy
    • Progress and Execution Logs
    • Job Outcomes
    • 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
      • Weasel Schema Management
    • 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

Microsoft's dependency injection container builds the scheduler. AddQuartz registers the object graph; the job store, thread pool, listeners, plugins and your jobs are resolved from the container. Nothing is built reflectively from type names.

This is in the core Quartz package; 3.x had it in the separate Quartz.Extensions.DependencyInjection package.

Tips

The hosted service starts and stops the scheduler with the application.

Need several independent schedulers in one application? See Multiple Schedulers.

Registering a scheduler

Install Quartz:

dotnet add package Quartz

Host.CreateApplicationBuilder and WebApplication.CreateBuilder come from Microsoft.Extensions.Hosting. The worker and web templates reference it; a plain console project needs it added:

dotnet add package Microsoft.Extensions.Hosting

ExampleJob, used throughout this page, is an ordinary class:

public sealed class ExampleJob : IJob
{
    private readonly ILogger<ExampleJob> logger;

    public ExampleJob(ILogger<ExampleJob> logger)
    {
        this.logger = logger;
    }

    // Job data is applied onto properties of matching name and type before Execute runs,
    // so each one needs a setter. Nothing here reads the job data map.
    public string InjectedString { get; set; } = "";

    public bool InjectedBool { get; set; }

    public ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        logger.LogInformation(
            "ExampleJob fired: InjectedString={InjectedString} InjectedBool={InjectedBool}",
            InjectedString,
            InjectedBool);

        return default;
    }
}
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.AddQuartz(q =>
{
    q.ScheduleJob<ExampleJob>(trigger => trigger
        .WithIdentity("example")
        // every ten seconds, so a first run reports itself quickly;
        // "0 0/5 * * * ?" is every five minutes
        .WithCronSchedule("0/10 * * * * ?"));
});

builder.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);

The file needs using Microsoft.Extensions.Hosting; and using Quartz;. The ten-second schedule shows quickly whether a first registration works.

AddQuartz and AddQuartzHostedService are on IHostApplicationBuilder, so they also work with WebApplication.CreateBuilder(args). Both are on IServiceCollection too (builder.Services.AddQuartz(q => …)), for code that has only the collection.

Configuration from appsettings.json

Everything configurable in code binds from the Quartz configuration section, under the option's own name:

{
  "Quartz": {
    "Scheduler": {
      "InstanceName": "Sample Scheduler",
      "MaxBatchSize": 5
    },
    "ThreadPool": {
      "MaxConcurrency": 20
    }
  }
}

Registration through IHostApplicationBuilder binds the Quartz section automatically. On IServiceCollection, pass the section:

services.AddQuartz(configuration.GetSection("Quartz"), q =>
{
    // code configuration on top of what the file says; code wins
    q.ConfigureScheduler(options => options.InstanceId = "Scheduler-Core");
});

Section names, their options and the schedule-in-configuration format are in Configuration Reference and JSON configuration. The 3.x flat keys (quartz.scheduler.instanceName style) still work and map to the same options; do not use them in a new application.

How jobs are constructed

Jobs are resolved from the container.

  • AddJob<T>(), AddJob(type, …) and ScheduleJob<T>() register the job type as a scoped service.
  • A scheduler with a job factory of your own registers none of its job types, whether UseJobFactory comes before or after them: your factory builds them. A factory derived from MicrosoftDependencyInjectionJobFactory resolves from the container, so its scheduler still registers them.
  • The job factory opens a scope per fire, resolves the job from it, and disposes the scope when the job returns, so a job can take scoped dependencies such as a database context.
  • A job type with no registration is built with ActivatorUtilities, so jobs from XML or JSON files work.
  • A job type with no registration and one public constructor, taking no parameters, needs nothing from the container: it is built without a scope, unless ConfigureJobScope is set, and disposed when it returns.
  • A job should have only one public constructor.

The registration is a TryAdd, so your own registration wins:

// your lifetime, your factory, your implementation type - kept
services.AddSingleton<SendReportsJob>(_ => SendReportsJob.ForTenant("acme"));

services.AddQuartz(q =>
{
    q.AddJob<SendReportsJob>(j => j.WithIdentity("send-reports"));
});

Warning

A singleton job serves every fire from one instance: it must be thread-safe and cannot take scoped dependencies. Prefer scoped, which is what AddJob registers.

A registered job may not take a scheduler's parts by constructor

IScheduler, ISchedulerFactory, IJobStore, IThreadPool and IOptions<QuartzSchedulerOptions> belong to one scheduler, but the container resolves them unkeyed, without knowing which scheduler fires the job. Startup refuses such a constructor, naming the job and the parameter. Instead:

  • use IJobExecutionContext.Scheduler for the scheduler running the fire;
  • read the firing from IJobExecutionContextAccessor where the context is not passed in;
  • register a job that must be constructed with a scheduler part with AddJobType<T>(provider => …), which resolves the part by key and is not checked.

See which scheduler's parts a job is built from.

To add to the factory's scope without replacing the factory (to seed an ambient tenant, say), use q.ConfigureJobScope((scope, bundle, scheduler) => …).

Failing fast when job dependencies cannot be resolved

The job type is registered, so ValidateOnBuild (on by default in the Development environment) checks its constructor. A job with an unregistered dependency fails when the container is built, naming the job and the dependency:

services.AddQuartz(q => q.AddJob<SendReportsJob>(j => j.WithIdentity("send-reports")));

// throws: Unable to resolve service for type 'IReportStore' while attempting to activate 'SendReportsJob'

Before 4.0 the job type was not registered, so the failure came at fire time: the trigger fired, the job never ran, and every trigger of the job moved to TriggerState.Error until IScheduler.ResetTriggerFromErrorState was called.

Unregistered jobs (named by an XML or JSON schedule, or built by your own job factory) can still fail that way. To react at fire time, for example to fail whatever scheduled the work, handle ISchedulerListener.SchedulerError. Its SchedulerErrorContext names the trigger, the job and the fire instance, and wraps a JobInstantiationException carrying the same three:

public sealed class InstantiationFailureListener(ILogger<InstantiationFailureListener> logger) : ISchedulerListener
{
    public ValueTask SchedulerError(IScheduler scheduler, SchedulerErrorContext errorContext, CancellationToken cancellationToken = default)
    {
        if (errorContext.Exception is JobInstantiationException failure)
        {
            logger.LogError(failure, "Job {Job} could not be built for trigger {Trigger}, fire {FireInstanceId}, on scheduler {SchedulerName}",
                errorContext.JobKey, errorContext.TriggerKey, errorContext.FireInstanceId, scheduler.SchedulerName);
        }

        return default;
    }
}

ISchedulerListener.TriggersInError is raised too: every trigger of that job is now in the error state.

To take part in construction (to record the failure or add context), derive from MicrosoftDependencyInjectionJobFactory and override CreateJobInstance. Its TriggerFiredBundle carries the trigger, the job detail and bundle.Trigger.FireInstanceId.

Persistent job stores

On every start, what you register is compared with the database and the stored schedule is updated to match.

Warning

With a persistent job store, always give jobs and triggers explicit names. Without one, each gets a new generated name on every start, the existence check finds nothing, and the schedule accumulates duplicates. A name is enough; the group defaults to the same value every time.

builder.Services.AddQuartz(q =>
{
    q.UsePersistentStore(store =>
    {
        store.UseSqlServer(connectionString);
        store.UseSystemTextJsonSerializer();

        store.ConfigureStore(options =>
        {
            options.TablePrefix = "QRTZ_";        // the default
            options.StoreJobDataAsStrings = true; // preferred, but not the default
            options.SchemaProvisioning = SchemaProvisioning.Validate; // the default
        });

        store.UseClustering(cluster =>
        {
            cluster.CheckinInterval = TimeSpan.FromSeconds(10);
            cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(20);
        });
    });
});

Store settings go through store.ConfigureStore(...), which configures AdoJobStoreOptions; the database and clustering calls are on the builder. Duplicate scheduling data has its own setting:

services.Configure<QuartzOptions>(options =>
{
    // Pass over a declared job or trigger whose key is already stored, rather than replacing it.
    // OverwriteExistingData defaults to true, and this turns that default off; writing both down
    // is asking for opposite things and is refused at startup.
    options.Scheduling.IgnoreDuplicates = true; // default: false
});

A worked configuration

One registration, in parts.

Jobs and triggers. ScheduleJob<T> registers a job with one trigger. AddJob plus AddTrigger registers a job shared by several triggers, each with its own data.

builder.Services.AddQuartz(q =>
{
    q.ScheduleJob<ExampleJob>(trigger => trigger
        .WithIdentity("Combined Configuration Trigger")
        .StartAt(DateTimeOffset.UtcNow.AddSeconds(7))
        .WithDailyTimeIntervalSchedule(x => x.WithInterval(10, IntervalUnit.Second))
        .WithDescription("my awesome trigger configured for a job with single call"));

    JobKey jobKey = new("awesome job", "awesome group");

    q.AddJob<ExampleJob>(j => j
        .WithIdentity(jobKey)
        .WithDescription("my awesome job")
        // job data can name the job property it is meant for instead of spelling its key,
        // which makes a mistyped key or a wrong-typed value a compile error
        .UsingJobData(x => x.InjectedString, "Hello")
        .UsingJobData(x => x.InjectedBool, true));

    q.AddTrigger<ExampleJob>(t => t
        .WithIdentity("Simple Trigger")
        .ForJob(jobKey)
        .StartNow()
        .WithSimpleSchedule(TimeSpan.FromSeconds(10)));

    q.AddTrigger<ExampleJob>(t => t
        .WithIdentity("Cron Trigger")
        .ForJob(jobKey)
        .StartAt(DateTimeOffset.UtcNow.AddSeconds(3))
        .WithCronSchedule("0/3 * * * * ?"));

    // use H (hash) to spread trigger fire times based on trigger identity
    q.AddTrigger<ExampleJob>(t => t
        .WithIdentity("Spread Cron Trigger")
        .ForJob(jobKey)
        .WithCronSchedule("H * * * * ?")
        .WithDescription("fires once per minute at a hash-derived second"));
});

Before each fire, MicrosoftDependencyInjectionJobFactory (derived from PropertySettingJobFactory) copies each entry of the merged job data map onto the job property of the same name. ExampleJob sees InjectedString set without reading context.MergedJobDataMap; UsingJobData(x => x.InjectedString, "Hello") writes that entry.

  • A property needs a setter, and its type must match the value.
  • An entry that matches no property is ignored by default; PropertyMismatchBehavior makes it a warning or a failure.
  • Reading the map directly still works, for values with no property.

Calendars, to exclude days from a schedule:

const string calendarName = "myHolidayCalendar";

q.AddCalendar<HolidayCalendar>(
    name: calendarName,
    options: new AddCalendarOptions { Replace = true, UpdateTriggers = true },
    configure: calendar => calendar.AddExcludedDay(new DateOnly(2026, 5, 15)));

q.AddTrigger<ExampleJob>(t => t
    .WithIdentity("Daily Trigger")
    .ForJob(jobKey)
    .WithDailyTimeIntervalSchedule(x => x.WithInterval(10, IntervalUnit.Second))
    .WithCalendarName(calendarName));

The generic overloads construct the calendar with new T(). For a calendar with a dependency (a holiday list from a database, a clock), pass a factory. It receives the scheduler-scoped service provider, so a named scheduler's calendar gets that scheduler's parts:

// A calendar that needs a dependency cannot be built by the generic overloads, which
// construct it with new T(). This one is handed the scheduler's service provider.
q.AddCalendar("businessDays", serviceProvider =>
{
    HolidayCalendar calendar = new() { TimeZone = TimeZoneInfo.Utc };
    foreach (DateOnly day in serviceProvider.GetRequiredService<IHolidayList>().Days)
    {
        calendar.AddExcludedDay(day);
    }

    return calendar;
});

Plugins, including a schedule kept in a file and watched for changes:

q.UseJsonSchedulingConfiguration(x =>
{
    x.Files.Add("~/quartz_jobs.json");
    x.ScanInterval = TimeSpan.FromSeconds(2);
    x.FailOnSchedulingError = true;
});

// resolve Windows and IANA time zone ids on either operating system
q.UseTimeZoneConverter();

Every shipped plugin has such an extension; see Plugins.

A timeout, a middleware in the core package:

// interrupt a job that runs longer than it should; a job saying [JobTimeout("00:00:05")]
// gets five seconds instead
q.AddJobTimeout(TimeSpan.FromMinutes(5));

q.ScheduleJob<SlowJob>(
    trigger => trigger
        .WithIdentity("slowJobTrigger")
        .StartNow()
        .WithSimpleSchedule(TimeSpan.FromSeconds(5)),
    job => job.WithIdentity("slowJob"));

Job Execution Middleware covers what a timeout does to the trigger and how a timed-out firing becomes a retryable failure.

Listeners, built by the container and in place before the scheduler starts:

q.AddSchedulerListener<SampleSchedulerListener>();
q.AddJobListener<SampleJobListener>(GroupMatcher<JobKey>.GroupEquals("awesome group"));
q.AddTriggerListener<SampleTriggerListener>();

Registration that depends on your own configuration. Decide whether to schedule in ordinary code; read values the trigger needs from the container when the trigger is built:

services.Configure<SampleOptions>(configuration.GetSection("Sample"));

services.AddQuartz(q =>
{
    if (!string.IsNullOrWhiteSpace(configuration.GetSection("Sample")["CronSchedule"]))
    {
        JobKey customJobKey = new("options-custom-job", "custom");

        q.AddJob<ExampleJob>(j => j.WithIdentity(customJobKey));

        q.AddTrigger<ExampleJob>((serviceProvider, trigger) => trigger
            .WithIdentity("options-custom-trigger", "custom")
            .ForJob(customJobKey)
            .WithCronSchedule(serviceProvider.GetRequiredService<IOptions<SampleOptions>>().Value.CronSchedule));
    }
});

Configuration that depends on a service. The builder callback runs before the container exists. These members have an overload that receives the service provider when the scheduler is built: AddJob, AddTrigger, ScheduleJob, AddCalendar, UseJobStore, AddPlugin, the three Add*Listener methods, AddJobMiddleware, UseExecutionLimits and AddJobTimeout.

services.AddOptions<SampleOptions>()
    .Bind(configuration.GetSection("Sample"))
    .Validate(options => options.MaxConcurrent > 0, "Sample:MaxConcurrent must be positive")
    .ValidateOnStart();

services.AddQuartz(q =>
{
    // Read when the scheduler is built, so the options have been bound, post-configured and
    // validated by the time the number is asked for.
    q.UseExecutionLimits((serviceProvider, limits) => limits.ForGroup(
        "reports",
        serviceProvider.GetRequiredService<IOptions<SampleOptions>>().Value.MaxConcurrent));

    q.AddJobTimeout(serviceProvider =>
        serviceProvider.GetRequiredService<IOptions<SampleOptions>>().Value.JobTimeout);
});

Reading the section yourself (configuration.GetSection("Sample").Get<SampleOptions>()) would skip every Configure, PostConfigure and validation registered for those options.

For any other setting, use the options pattern with the service it depends on. A scheduler's options are a named instance whose name is the scheduler's name:

services.AddQuartz("reporting", q => q.UsePersistentStore(store => store.UseSqlServer("...")));

// The scheduler's name is its options name, so a named scheduler is configured under it.
services.AddOptions<AdoJobStoreOptions>("reporting")
    .Configure<ITablePrefixSource>((options, source) => options.TablePrefix = source.TablePrefix);
Help us by improving this page!
Last Updated: 9/30/26, 6:00 AM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
Hosted Services Integration
Next
Multiple Schedulers with Microsoft DI