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

Quartz.Plugins provides ready-made scheduler plugins: scheduling jobs from a file at startup, and logging job and trigger history. A plugin implements ISchedulerPlugin, in the Quartz.Extensibility namespace.

The plugins are in the Quartz.Plugins.History, Quartz.Plugins.Json and Quartz.Plugins.Xml namespaces, matching the assembly and package name. In 3.x they were the singular Quartz.Plugin.*; a quartz.plugin.<name>.type with the old spelling still resolves, with a warning.

Installation

dotnet add package Quartz.Plugins

Configuration

Add and configure each plugin with its extension method. The 3.x flat keys, quartz.plugin.{name-to-refer-with}.{property}, still work.

PluginExtensionOptions
LoggingJobHistoryPluginUseJobHistoryLogging(…)JobHistoryLoggingOptions
LoggingTriggerHistoryPluginUseTriggerHistoryLogging(…)TriggerHistoryLoggingOptions
StructuredLoggingJobHistoryPluginUseStructuredJobLogging(…)JobHistoryLoggingOptions
StructuredLoggingTriggerHistoryPluginUseStructuredTriggerLogging(…)TriggerHistoryLoggingOptions
JsonSchedulingDataProcessorPluginUseJsonSchedulingConfiguration(…)FileSchedulingOptions
XmlSchedulingDataProcessorPluginUseXmlSchedulingConfiguration(…)FileSchedulingOptions

The extensions are on IQuartzBuilder, so they work under AddQuartz and inside QuartzSchedulerBuilder.Create(q => …). How a plugin is registered and named is in the configuration reference.

FileSchedulingOptions, for the two schedule-file plugins:

OptionTypeDefaultDescription
FilesList<string>emptyFiles to read the schedule from; get-only, so add to it
FailOnFileNotFoundbooltrueA missing file stops the scheduler from starting
FailOnSchedulingErrorboolfalsefalse: contents that cannot be scheduled are logged and skipped; true: startup fails
ScanIntervalTimeSpan00:00:00How often files are re-read; zero means once, at startup

With ScanInterval zero, a changed file needs a restart.

JobHistoryLoggingOptions and TriggerHistoryLoggingOptions hold only message templates, each defaulting to null (the plugin's own). LoggingJobHistoryPlugin shows their shape.

These options types are the scheduler's named options, so a configuration section binds onto them:

// A plugin's options are the scheduler's own named options, so a configuration section binds
// onto them like any other. The callback below is applied over whatever the section said.
services.Configure<FileSchedulingOptions>(configuration.GetSection("Quartz:Json"));

services.AddQuartz(q => q.UseJsonSchedulingConfiguration(x => x.ScanInterval = TimeSpan.FromMinutes(1)));

Sources apply in this order, the last winning:

  1. the flat quartz.plugin.{name}.{property} keys;
  2. values bound onto the options;
  3. the callback passed to the extension method.

The callback overrides only the settings it sets. Under AddQuartz("name", …) the options are that scheduler's: bind them with services.Configure<TOptions>("name", section).

Features

LoggingJobHistoryPlugin

Logs every job execution and execution veto to the configured logging infrastructure. LoggingTriggerHistoryPlugin does the same for trigger firings, misfires and completions.

services.AddQuartz(q =>
{
    q.UseJobHistoryLogging(options =>
    {
        // each message left unset keeps the plugin's own default
        options.JobSuccessMessage = "Job {1}.{0} completed";
    });

    q.UseTriggerHistoryLogging();
});

Both use index-based placeholders. Prefer the structured plugins below unless you have existing message templates to keep.

StructuredLoggingJobHistoryPlugin

The structured alternative to LoggingJobHistoryPlugin. It uses named template parameters (such as {JobName}, {TriggerGroup}) instead of index-based placeholders, so structured sinks such as Serilog and NLog can use the output. It also avoids the template cache memory leaks the original plugin can cause.

Parameters are mapped by position: a customized template must keep them in the default order.

PropertyParameters (in order)
JobToBeFiredMessage{JobGroup}, {JobName}, {TriggerGroup}, {TriggerName}, {FireTime}, {ScheduledFireTime}, {NextFireTime}, {RefireCount}
JobSuccessMessage{JobGroup}, {JobName}, {FireTime}, {TriggerGroup}, {TriggerName}, {Result}
JobFailedMessage{JobGroup}, {JobName}, {FireTime}, {TriggerGroup}, {TriggerName}, {ExceptionMessage}
JobWasVetoedMessage{JobGroup}, {JobName}, {TriggerGroup}, {TriggerName}, {FireTime}

DI configuration:

services.AddQuartz(q =>
{
    q.UseStructuredJobLogging(options =>
    {
        // Optional; each template left unset keeps the plugin's own default.
        options.JobFailedMessage = "Job {JobGroup}.{JobName} failed: {ExceptionMessage}";
    });
});

Tips

Recommended over LoggingJobHistoryPlugin with structured logging providers (Serilog, NLog, etc.).

StructuredLoggingTriggerHistoryPlugin

The structured alternative to LoggingTriggerHistoryPlugin: logs trigger firings, misfires and completions with named template parameters. Parameters are mapped by position: a customized template must keep them in the default order.

PropertyParameters (in order)
TriggerFiredMessage{TriggerGroup}, {TriggerName}, {JobGroup}, {JobName}, {FireTime}, {ScheduledFireTime}, {NextFireTime}, {RefireCount}
TriggerMisfiredMessage{TriggerGroup}, {TriggerName}, {JobGroup}, {JobName}, {FireTime}, {ScheduledFireTime}, {NextFireTime}
TriggerCompleteMessage{TriggerGroup}, {TriggerName}, {JobGroup}, {JobName}, {CompletedTime}, {ScheduledFireTime}, {NextFireTime}, {TriggerInstructionCode}

DI configuration:

services.AddQuartz(q =>
{
    q.UseStructuredTriggerLogging(options =>
    {
        // Optional; each template left unset keeps the plugin's own default.
        options.TriggerMisfiredMessage = "Trigger {TriggerGroup}.{TriggerName} misfired at {FireTime}";
    });
});

Tips

Recommended over LoggingTriggerHistoryPlugin with structured logging providers (Serilog, NLog, etc.).

JsonSchedulingDataProcessorPlugin

Loads jobs and triggers from JSON files when the scheduler initializes, and can re-scan the files for changes. JSON is the maintained scheduling-file format: it gains each new trigger shape, while the XML trigger kinds are frozen at three. Every trigger setting has a field, including the preferred node; see Common Trigger Fields.

Warning

Periodic scanning for file changes is not supported in a clustered environment.

DI configuration:

services.AddQuartz(q =>
{
    q.UseJsonSchedulingConfiguration(x =>
    {
        x.Files.Add("quartz_jobs.json");
        x.ScanInterval = TimeSpan.FromMinutes(1);
        x.FailOnSchedulingError = true;
    });
});

For one file, read once, pass just the file name:

services.AddQuartz(q => q.UseJsonSchedulingConfiguration("quartz_jobs.json"));

The shorthand adds to Files, so it combines with the callback form and with itself. UseXmlSchedulingConfiguration has the same pair.

The file format and trigger types are in JSON Configuration.

XmlSchedulingDataProcessorPlugin

The XML twin of JsonSchedulingDataProcessorPlugin: loads jobs and triggers from XML files at initialization and can re-scan them, with the same settings. The format is frozen at three trigger kinds.

services.AddQuartz(q =>
{
    q.UseXmlSchedulingConfiguration(x =>
    {
        x.Files.Add("~/quartz_jobs.config");
        x.ScanInterval = TimeSpan.FromMinutes(1);
        x.FailOnSchedulingError = true;
    });
});

Warning

Periodic scanning for file changes is not supported in a clustered environment.

A file that declares the same job or trigger key (name and group) twice is rejected, whatever its <processing-directives>. <overwrite-existing-data> and <ignore-duplicates> govern the file against the scheduler, not against itself. See ProcessingDirectives; it applies to both formats.

The XML trigger kinds are frozen

job_scheduling_data_2_0.xsd, the schema for XML scheduling files, declares three trigger kinds: simple, cron and calendar-interval. It will not gain a fourth.

To scheduleXMLJSON
a simple, cron or calendar-interval trigger<simple>, <cron>, <calendar-interval>Simple, Cron, CalendarInterval
a daily time interval triggernot expressible, and will not beDailyTimeInterval
a recurrence triggernot expressible, and will not beRecurrence
a trigger in an execution group<execution-group>ExecutionGroup
a trigger with a retry policy<retry-policy>RetryPolicy
a trigger with a preferred node<preferred-node>PreferredNode
a continuation (waits for another trigger's firing)<continues-after>, <continuation-condition>ContinuesAfter, ContinuationCondition
a trigger with an overlap policy<overlap-policy>OverlapPolicy
  • XML scheduling is not deprecated and is not going away in 4.x. A quartz_jobs.xml that worked on 3.x works here.
  • Write a schedule that needs another trigger kind as JSON. Move an XML schedule to JSON when it needs a shape the schema cannot express.

A new trigger kind needs its own parser, misfire vocabulary and schema branch, so it is added to JSON only. The bottom four rows are settings on an existing trigger, not new kinds, so XML has an optional element for each. The first three were added in 4.1 and the continuation in 4.2, all between <calendar-name> and <job-data-map>. The schema keeps its 2.0 version and its http://quartznet.sourceforge.net/JobSchedulingData namespace, and a file written before these elements existed still validates with the same meaning:

<trigger>
  <cron>
    <name>nightlyReport</name>
    <job-name>reportJob</job-name>
    <calendar-name>holidays</calendar-name>
    <execution-group>batch</execution-group>
    <retry-policy>fixed;3;00:00:30</retry-policy>
    <preferred-node>production-node-1</preferred-node>
    <continues-after>
      <name>import</name>
      <group>nightly</group>
    </continues-after>
    <continuation-condition>OnFailure|OnCancellation</continuation-condition>
    <cron-expression>0 0 2 * * ?</cron-expression>
  </cron>
</trigger>
  • Order matters. The schema is a sequence, so the five elements go where shown above.
  • <preferred-node> takes a scheduler instance id, or * for an automatic pin.
  • <retry-policy> takes the policy's stored form.
  • An unreadable value is refused when the file is read, naming the trigger.

<continues-after> names a trigger like <delete-trigger> does: a <name> and an optional <group> (default DEFAULT). The trigger is stored waiting for that trigger's next firing instead of being scheduled.

  • <continuation-condition> lists the outcomes that release the wait, joined with |. Default: OnSuccess.
  • The parent is named, not resolved when the file is read. It may be declared later in the same file (the file's triggers are stored parent first) or already be in the store.
  • A parent in neither place is refused when the file is scheduled, with ObjectDoesNotExistException.
  • An unknown outcome, or a condition without <continues-after>, is refused when the file is read.

JobInterruptMonitorPlugin — retired

JobInterruptMonitorPlugin was removed in 4.0. Use AddJobTimeout(…) in the core Quartz package, a middleware that needs no JobDataMap keys:

builder.AddQuartz(q =>
{
    // every job gets five minutes, unless it says otherwise
    q.AddJobTimeout(TimeSpan.FromMinutes(5));

    // or: no scheduler-wide budget, and only the jobs carrying [JobTimeout] are bounded
    q.AddJobTimeout();
});

A job sets its own budget with an attribute, like [DisallowConcurrentExecution]:

// Thirty seconds for this job, whatever the scheduler's default is.
[JobTimeout("00:00:30")]
public sealed class ReportJob : IJob
{
    public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        // Forward the token: a job that never looks at it cannot be stopped by anything, and is simply
        // reported as having timed out once it finally returns.
        await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
    }
}

// No timeout at all, whatever the scheduler's default is.
[JobTimeout("00:00:00")]
public sealed class NightlyRebuildJob : IJob
{
    public ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default) => default;
}

The plugin's JobDataMap keys, "AutoInterruptable" and "MaxRunTime", are now ignored; delete them from job and trigger data. See the migration guide for before and after, and Job Execution Middleware for what a timeout does to the trigger.

ShutdownHookPlugin — retired

ShutdownHookPlugin, UseShutdownHook and ShutdownHookOptions were removed in 4.0. The plugin's async void handler on AppDomain.CurrentDomain.ProcessExit was never awaited, so the process could exit mid-Shutdown.

Under a host, the hosted service stops every registered scheduler as part of the application's shutdown, awaited. QuartzHostedServiceOptions.WaitForJobsToComplete replaces CleanShutdown:

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

Without a host, shut the scheduler down on the application's own exit path (the end of Main, a Ctrl+C handler, a scope's disposal), where the shutdown can be awaited:

await using StandaloneSchedulerFactory schedulerFactory = QuartzSchedulerBuilder.Create().Build();
IScheduler scheduler = await schedulerFactory.GetScheduler();
await scheduler.Start();

// ... the application runs ...

await scheduler.Shutdown(waitForJobsToComplete: true);

Adding a plugin

AddPlugin has the same three shapes as listener registration: the container builds the plugin, you build it, or it takes options you configure.

services.AddQuartz(q =>
{
    // the container constructs it, so it gets constructor injection
    q.AddPlugin<MyPlugin>();

    // you construct it
    q.AddPlugin(provider => new MyPlugin(provider.GetRequiredService<IMyPluginDependency>()));

    // it takes an IOptions<MyPluginOptions> of its own
    q.AddPlugin<MyPlugin, MyPluginOptions>(options => options.SomeSetting = "value");
});

Every shape takes an optional name as its last argument:

q.AddPlugin<MyPlugin>("myPlugin");
q.AddPlugin(provider => new MyPlugin(), "myPlugin");
q.AddPlugin<MyPlugin, MyPluginOptions>(options => options.SomeSetting = "value", "myPlugin");
  • The name is how the scheduler refers to the plugin. Some plugins derive persisted job and trigger keys from it, so keep it stable across deployments.
  • A quartz.plugin.{name}.* key configures the plugin with that name, so a plugin added in code can be configured from a file.
  • Default: the plugin's type name. The shipped plugins use short names (xml, json, jobHistory, …).

Options of the third shape belong to the scheduler they were added to, so two schedulers can add the same plugin with the same options type and different values. They are named options under the scheduler's name: a plugin on services.AddQuartz("reporting", …) is also configured by services.Configure<MyPluginOptions>("reporting", …). A plain services.Configure<MyPluginOptions>(…) configures the default scheduler's.

InjectUse
IOptions<MyPluginOptions>A fixed value
IOptionsMonitor<MyPluginOptions>Following a reloading configuration source

On IOptionsMonitor, CurrentValue is your scheduler's instance, Get(name) is the named one, and OnChange fires for your scheduler's options only.

Authoring plugin configuration extensions

ISchedulerPlugin has three members; only Initialize is required. Start and Shutdown default to doing nothing, for a plugin that does all its work at initialization (attaching a listener, registering a resolver). Implement them for work that needs a running scheduler, or resources to release on stop.

Give your plugin an extension method on IQuartzBuilder, like the built-in ones: take an options object, apply it to the plugin, and register the plugin under its conventional name.

public static class MyPluginConfigurationExtensions
{
    public static IQuartzBuilder UseMyPlugin(
        this IQuartzBuilder builder,
        Action<MyPluginOptions>? configure = null)
    {
        ArgumentNullException.ThrowIfNull(builder);

        var options = new MyPluginOptions();
        configure?.Invoke(options);

        // companion services your plugin needs injected
        builder.Services.TryAddSingleton<IMyPluginDependency, MyPluginDependency>();

        return builder.AddPlugin<MyPlugin>(
            provider =>
            {
                var plugin = ActivatorUtilities.CreateInstance<MyPlugin>(provider);
                plugin.SomeSetting = options.SomeSetting;
                return plugin;
            },
            name: "myPlugin");
    }
}

public sealed class MyPluginOptions
{
    public string? SomeSetting { get; set; }
}

It works wherever an IQuartzBuilder does, in both configuration styles:

// under a host
services.AddQuartz(q => q.UseMyPlugin(options => options.SomeSetting = "value"));

// standalone, without an application container — the same callback, a different receiver
IScheduler scheduler = await QuartzSchedulerBuilder
    .Create(q => q.UseMyPlugin(options => options.SomeSetting = "value"))
    .BuildScheduler();

This extension and a quartz.plugin.myPlugin.someSetting key configure the same plugin instance, because they use the same name. The properties are applied to the registered plugin; no second copy is built.

Help us by improving this page!
Last Updated: 9/30/26, 6:00 AM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
JSON Serialization