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

Version compatibility

This documentation relates to Quartz version 4.2 and later.

Quartz ships an analyzer that reports, at build time, mistakes that otherwise throw at run time:

MistakeOtherwise throws
a cron expression that cannot parse, in AddQuartzwhile the host starts
the same, in ScheduleJobwhen the scheduling code runs
[JobTimeout("5 minutes")]the first time the timeout middleware reflects over the job, after deployment
[SimpleTrigger("10 minutes")]never: the generator cannot write the schedule, so the job silently lacks it

The analyzer is inside the package, under analyzers/dotnet/cs, so referencing Quartz is enough:

<PackageReference Include="Quartz" Version="4.2.0" />

It reads cron with the same parser Quartz uses at run time. H, wrapping ranges, L, W, #, the @ macros and the five-field Unix dialect mean the same thing, and the compiler prints the message the exception would have carried.

What it checks

IdSeverityWhat it says
QZ0001ErrorA cron literal or constant that the parser refuses
QZ0002ErrorA [JobTimeout] argument that is not a TimeSpan, or is negative
QZ0003WarningA job that persists its data map and allows concurrent firings
QZ0004InfoA job body that awaits or loops without reading its cancellation token
QZ0005ErrorA [SimpleTrigger] interval that is not a positive TimeSpan, or a RepeatCount below -1
QZ0006ErrorA [RetryPolicy] delay that is not a TimeSpan, or is negative

Only values the compiler already knows are checked: a string literal, a const, an interpolated string with nothing interpolated. A value built at run time, read from configuration or passed in a variable is skipped silently, rather than guessed at.

To change a severity or turn a check off, see Changing a severity, or turning it off.

QZ0001 InvalidCronExpression

Reports a cron string that cannot parse, at every call that takes one: CronScheduleBuilder.Create, WithCronSchedule, the CronExpression constructors, CronExpression.Parse, TryParse, ParseWithHash, TryParseWithHash and ResolveHash, the CronCalendar constructors and CronTriggerImpl's. Also on the [CronTrigger] attribute.

// error QZ0001: '0 0 12 * *' is not a valid cron expression: ... has 5 fields, but 6 or 7 are
// required: seconds, minutes, hours, day-of-month, month, day-of-week, and optionally year.
_ = TriggerBuilder.Create().WithCronSchedule("0 0 12 * *").Build();

The call decides how the literal is read:

  • The dialect. A sibling CronFormat argument is honoured: CronExpression.Parse("30 4 * * 1", CronFormat.Unix) is a valid crontab line, and the same five fields without it are not. A CronFormat the compiler cannot evaluate skips the check, because the same digits mean different days in each dialect.
  • Whether H is resolved. WithCronSchedule("0 H 3 * * ?") is valid: the builder resolves the hash against the trigger key later. CronExpression.Parse("0 H 3 * * ?") is not: it has no key. The analyzer reports what each entry point would throw.
  • TryParse is checked too. A literal that can never parse makes a TryParse that can only return false, which is a bug.
  • Missing expressions. A constant that is null, empty or whitespace is reported as missing: error QZ0001: The cron expression is missing: the argument is null. A null is allowed only where the parameter is string?: TryParse and TryParseWithHash return false for it; every other entry point throws.

Fix the expression, pass the CronFormat it is written in, or use an entry point that resolves H. Suppress with dotnet_diagnostic.QZ0001.severity in .editorconfig.

QZ0002 InvalidJobTimeout

Reports a [JobTimeout] argument that is not an invariant TimeSpan string, or is negative. The argument is a string because an attribute argument cannot be a TimeSpan, and nothing parses it until the timeout middleware reflects over the job type.

[JobTimeout("5 minutes")] // error QZ0002: '5 minutes' is not a TimeSpan. Spell the job's timeout the
public class ReportJob : IJob // way TimeSpan does, invariantly: "00:05:00" for five minutes.

Fix by spelling the budget as TimeSpan does: "00:05:00" for five minutes. "00:00:00" is not reported: zero means the job has no timeout, whatever the scheduler's default. Suppress with dotnet_diagnostic.QZ0002.severity in .editorconfig.

QZ0003 PersistJobDataWithoutDisallowConcurrent

Reports a job with [PersistJobDataAfterExecution] and without [DisallowConcurrentExecution]. The data map is re-stored when a firing completes; with concurrent firings, two read the same map, change it independently and store it in turn, so the first write is silently lost.

[PersistJobDataAfterExecution]
[DisallowConcurrentExecution] // without this line: warning QZ0003
public class CounterJob : IJob

Both attributes are read as Quartz reads them at run time: on the type, a base class, or any interface it implements. A job that gets [DisallowConcurrentExecution] from an interface is not reported; one that inherits [PersistJobDataAfterExecution] from one is.

Fix by adding [DisallowConcurrentExecution]. Suppress with dotnet_diagnostic.QZ0003.severity in .editorconfig.

QZ0004 CancellationTokenNotObserved

Reports a job body that awaits or loops and never reads its cancellation token: neither the parameter nor IJobExecutionContext.CancellationToken, which is the same token. The scheduler cancels it on shutdown and on IScheduler.Interrupt; a body that ignores it runs to completion regardless.

public async ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
{
    await Task.Delay(TimeSpan.FromMinutes(5)); // info QZ0004
}
  • The body read is the one the scheduler runs for IJob.Execute: the implementation (explicit or not), an override in a job deriving from a virtual or abstract base, or the half of a partial method with the body. An Execute that hides the base job's with new is not read.
  • It is information, not a warning: whether work is interruptible is a judgement no analyzer can make, and a job that returns in a millisecond is right to ignore the token. The await-or-loop condition keeps it off jobs that could not honour a cancellation anyway.
  • CA2016, shipped with the .NET SDK, covers the related case: a token that is not forwarded to a call that takes one.

Fix by reading the token: pass it to what the job awaits, or check it in the loop. Suppress with dotnet_diagnostic.QZ0004.severity in .editorconfig, as in the example below.

QZ0005 InvalidSimpleTriggerSchedule

Reports a [SimpleTrigger] (4.3) whose interval is not a positive invariant TimeSpan string, or whose RepeatCount is below -1. The generator writes the interval into the registration as ticks, so it cannot write a schedule it cannot read.

[QuartzJob]
[SimpleTrigger("10 minutes")] // error QZ0005: '10 minutes' is not a TimeSpan. Spell the trigger's
public class PollJob : IJob   // interval the way TimeSpan does, invariantly: "00:10:00" for ten minutes.
WrittenReported as
"10 minutes", ""not a TimeSpan
"00:00:00", "-00:10:00"not longer than zero
nullmissing
RepeatCount = -2below -1; -1 repeats forever

Fix by spelling the interval as TimeSpan does: "00:10:00" for ten minutes. A reported schedule is not generated, so with the severity lowered the job builds without it. Suppress with dotnet_diagnostic.QZ0005.severity in .editorconfig.

QZ0006 InvalidRetryPolicyDelay

Reports a [RetryPolicy] (4.4) delay that is not an invariant TimeSpan string, or is negative: the second argument of the fixed and exponential forms, each argument of the explicit form, and MaxDelay. Nothing parses it until the job is added to a scheduler, which then refuses the job.

[RetryPolicy(3, "5 minutes")] // error QZ0006: '5 minutes' is not a TimeSpan. Spell a retry delay the
public class ImportJob : IJob  // way TimeSpan does, invariantly: "00:00:30" for thirty seconds.

Fix by spelling the delay as TimeSpan does: "00:05:00" for five minutes. The attempt count, the factor and the jitter are not checked here; the attribute refuses them when the job is added. Suppress with dotnet_diagnostic.QZ0006.severity in .editorconfig.

Changing a severity, or turning it off

Each diagnostic is an ordinary compiler diagnostic, so .editorconfig sets its severity:

[*.cs]
# A cron literal that cannot parse is only a warning here
dotnet_diagnostic.QZ0001.severity = warning

# This codebase does not want the cancellation-token hint
dotnet_diagnostic.QZ0004.severity = none

To remove the analyzer from the build entirely, set one property in the project file:

<PropertyGroup>
  <DisableQuartzAnalyzers>true</DisableQuartzAnalyzers>
</PropertyGroup>

Tips

The property removes the whole assembly, so the source generator and its AddDeclaredJobs() go with the five diagnostics, and delegate jobs are bound by reflection; nothing else Quartz does is affected. To quiet one rule, use .editorconfig instead. ExcludeAssets="analyzers" on the package reference does not work: the .NET 10 SDK still passes the assembly to the compiler.

What it does not check

  • RRULE literals (RecurrenceScheduleBuilder.Create). The recurrence parser is not BCL-clean, so it cannot be linked into an assembly the compiler loads.
  • Time-zone ids (TimeZones.FindById). The build machine's zone database is not the run-time machine's; refusing a zone your server has would be worse than no check.
  • Stored-form retry policies, a [RetryPolicy]'s counts and factor, preferred nodes, key names and type-name strings. Each is rare in code, refused by name at startup, or already [RequiresUnreferencedCode].

Related

Declaring Jobs with Attributes covers the rest of the 4.2 compile-time work: attributes that declare a job's schedule on the job class, and a source generator, in the same assembly, that registers them. Its diagnostics are QZ1001 to QZ1005; the cron expression on an attribute is checked by QZ0001 above, and the interval by QZ0005.

Help us by improving this page!
Last Updated: 10/6/26, 2:13 PM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
Testing
Next
Declaring Jobs with Attributes