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
      • Dashboard Agent
      • 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.Jobs provides ready-made utility jobs, such as sending e-mail and running native processes. The namespace, assembly and NuGet package are all Quartz.Jobs. In 3.x the namespace was the singular Quartz.Job; a configuration string or stored JOB_CLASS_NAME with the old spelling still resolves, with a warning.

Installation

dotnet add package Quartz.Jobs

How these jobs are configured

Each job reads its settings from its JobDataMap, under the keys listed with it below. The keys are the persisted form: what a job store writes, a cluster shares, and an XML or JSON scheduling file names.

Each job also has an options type that maps to exactly those keys, and an extension that writes it. The stored job is the same; the options type prevents misspelled keys and wrongly typed values.

JobOptionsExtension
DirectoryScanJobDirectoryScanOptionsUsingDirectoryScanOptions(…)
FileScanJobFileScanOptionsUsingFileScanOptions(…)
NativeJobNativeJobOptionsUsingNativeJobOptions(…)
SendMailJobSendMailOptionsUsingSendMailOptions(…)

The extensions work on JobBuilder.Create<TJob>() and on the configurator AddJob<TJob>(…) passes you, and return the same builder so the chain continues. Options.FromJobData(map) reads the settings back from a job's data.

Features

DirectoryScanJob

Scans directories for files whose last-modified time changed since the previous scan. When files were updated, created or deleted, it calls an IDirectoryScanListener.

IJobDetail job = JobBuilder.Create<DirectoryScanJob>()
    .WithIdentity("inboxScan")
    .UsingDirectoryScanOptions(new DirectoryScanOptions
    {
        Directories = ["/var/spool/inbox"],
        ScanListenerName = nameof(InboxListener),
        SearchPattern = "*.csv",
        IncludeSubDirectories = true,
        MinimumUpdateAge = TimeSpan.FromSeconds(30),
    })
    .Build();
SettingJob data keyDefault
DirectoriesDIRECTORY_NAMES (semicolon-separated), or DIRECTORY_NAME for one—
DirectoryProviderNameDIRECTORY_PROVIDER_NAMEnone; the paths above are used
ScanListenerNameDIRECTORY_SCAN_LISTENER_NAMErequired
SearchPatternSEARCH_PATTERN*
IncludeSubDirectoriesINCLUDE_SUB_DIRECTORIESfalse
MinimumUpdateAgeMINIMUM_UPDATE_AGE, in milliseconds5 seconds

MinimumUpdateAge is how long a file must be unchanged before it is reported, so a file another process is still writing is not handed over half-finished.

The listener is resolved in this order:

  1. A keyed registration: AddKeyedSingleton<IDirectoryScanListener>("inbox", …), and ScanListenerName = "inbox".
  2. Dependency injection by type name: register your implementation as IDirectoryScanListener (AddSingleton<IDirectoryScanListener, InboxListener>()) and set ScanListenerName = nameof(InboxListener).
  3. SchedulerContext: store the instance under a key, and name that key.

Registering the concrete type alone is no longer enough

Until 4.0 rc.1 the name was resolved by sweeping every loaded assembly with GetTypes(), so AddSingleton<InboxListener>() was found, and so was a same-named type from any other assembly. Neither is found now. Register the listener as IDirectoryScanListener, or key it.

scheduler.Context["inboxListener"] = new InboxListener();

The scheduler context is not a secret store

GET {ApiPath}/schedulers/{name}/context returns every entry, falling back to Convert.ToString. For a record or struct with a compiler-generated ToString, that is every field. Any authorized caller can read the context, as with a job's data map. Store a shared instance or a name there; keep connection strings and API keys in IConfiguration, a key vault or the container.

To choose directories at run time, implement IDirectoryProvider, store the instance in the SchedulerContext, and set DirectoryProviderName to that key. It receives the merged job data and returns the paths to scan.

The job keeps the last modification time and file list it saw in the job detail's data map, so it is [PersistJobDataAfterExecution]. The file list is a Dictionary<string, string> of full path to last-write ticks under CURRENT_FILE_LIST, which both shipped serializers accept. Before 4.0 rc.1 it was a List<FileInfo>, which neither can read back: the first firing against a persistent store failed to persist, and the HTTP API refused to read the job's data map.

FileScanJob

Checks one file's last-modified time. When it changed since the previous check, the job calls an IFileScanListener found in the SchedulerContext.

IJobDetail job = JobBuilder.Create<FileScanJob>()
    .WithIdentity("configWatch")
    .UsingFileScanOptions(new FileScanOptions
    {
        FileName = "/etc/app/settings.json",
        ScanListenerName = "settingsListener",
        MinimumUpdateAge = TimeSpan.FromSeconds(5),
    })
    .Build();
SettingJob data keyDefault
FileNameFILE_NAMErequired
ScanListenerNameFILE_SCAN_LISTENER_NAMErequired
MinimumUpdateAgeMINIMUM_UPDATE_AGE, in milliseconds5 seconds

NativeJob

Runs a native executable in a separate process.

IJobDetail job = JobBuilder.Create<NativeJob>()
    .WithIdentity("dumbJob")
    .UsingNativeJobOptions(new NativeJobOptions
    {
        Command = "echo",
        Parameters = "\"hi\" >> foobar.txt",
    })
    .Build();

ITrigger trigger = TriggerBuilder.Create()
    .WithIdentity("dumbTrigger")
    .WithSimpleSchedule(x => x.WithInterval(TimeSpan.FromSeconds(5)).RepeatForever())
    .Build();

await scheduler.ScheduleJob(job, trigger);
SettingJob data keyDefault
Commandcommandrequired
Parametersparametersnone
WaitForProcesswaitForProcesstrue
ConsumeStreamsconsumeStreamsfalse
WorkingDirectoryworkingDirectorythe scheduler's
  • With WaitForProcess on, the process exit code is the job execution result in the IJobExecutionContext.
  • Turn ConsumeStreams on for a process with a lot of output: one that fills its pipe blocks until the output is read.

Referencing this package changes what an open scheduling endpoint means

The HTTP API and the dashboard schedule a job whose type is a string from the request. It is resolved later with Type.GetType against the host's probing path. There is no allow-list; only the shape of the name is validated. Once Quartz.Jobs is referenced, NativeJob is on that path, and it runs the executable and arguments its job data names. An unauthenticated Quartz endpoint in such a process is remote code execution.

You may have this package without referencing it. Quartz.Plugins depends on Quartz.Jobs, so an application using the plugins (for XML scheduling, say) has NativeJob on its probing path. Check your restored package graph, not your project file.

Neither surface starts when its mapping says nothing about authorization. DirectoryScanJob and FileScanJob also take their paths from job data, and SendMailJob reads an SMTP credential from job data unless one is registered; see Keep the SMTP credential out of job data.

SendMailJob

Sends an e-mail.

IJobDetail job = JobBuilder.Create<SendMailJob>()
    .WithIdentity("nightlyDigest")
    .UsingSendMailOptions(new SendMailOptions
    {
        SmtpHost = "smtp.example.com",
        SmtpPort = 587,
        Sender = "[email protected]",
        Recipient = "[email protected]",
        Subject = "Nightly digest",
        Message = "Everything ran.",
    })
    .Build();
SettingJob data keyDefault
SmtpHostsmtp_hostrequired
SmtpPortsmtp_portthe client's default
Sendersenderrequired
Recipientrecipientrequired
CcRecipientcc_recipientnone
ReplyToreply_tothe sender
Subjectsubjectrequired
Messagemessagerequired
Encodingencodingthe default
EnableSslsmtp_enable_sslfalse

Override Send(MailInfo, CancellationToken) to send through something other than SmtpClient, or BuildMessage(SendMailOptions) to add an attachment or header before sending.

This job is an authenticated relay for whoever can schedule it

Sender, Recipient, Subject, Message and SmtpHost are all caller data. Anyone who can schedule a job can send mail from any address to any address through your server.

EnableSsl is off by default, as in SmtpClient. Turning it on fails against a server that does not offer TLS, such as an existing relay on the same host. Turn it on for anything that crosses a network you do not own, and for anything that authenticates: SMTP AUTH LOGIN is base64, not encryption.

Keep the SMTP credential out of job data

SendMailOptions has no user name or password on purpose. A persistent job store writes job data to QRTZ_JOB_DETAILS; every cluster node reads it, the dashboard shows it, and any export of the table carries it.

Register the credential with the container instead, bound to its server:

// Bound to the server it belongs to. The host to send through is job data, so a credential that
// answers for every host would go to whatever that data names.
CredentialCache credentials = new();
credentials.Add("smtp.example.com", 587, "Basic", new NetworkCredential("mailer", smtpPassword));

services.AddSingleton<ICredentialsByHost>(credentials);

Use CredentialCache, for security. smtp_host is job data, chosen by whoever scheduled the job. A bare NetworkCredential answers ICredentialsByHost.GetCredential with itself for every host, which would send the login, as base64 AUTH LOGIN, to any host the job data names.

RegisteredResult
CredentialCache with an entry for the job's hostThat entry is used
CredentialCache with no entry for itMail is sent unauthenticated
Bare NetworkCredentialThe job refuses to send, naming the host and how to bind the credential
Your own ICredentialsByHostAsked GetCredential(host, port, "Basic"), then "login"; its answer is used

Keep the password with your other secrets (user secrets in development, a key vault or environment variable in production) and pass it in through IConfiguration.

When nothing is registered, the smtp_username and smtp_password job data keys are still read, so jobs from earlier versions keep sending. The rule above does not apply to them, since the same data names the host. The job logs a warning when it uses them, and a registered credential wins.

NoOpJob

Does nothing. Use it as a placeholder, or to fire listeners on a schedule with no work attached.

Registering these jobs with the container

The jobs take their dependencies (TimeProvider, IServiceProvider, ICredentialsByHost) from the container. Register them like your own jobs:

builder.Services.AddQuartz(q =>
{
    q.AddJob<NativeJob>(j => j
        .WithIdentity("nightlyReport")
        .StoreDurably()
        .UsingNativeJobOptions(new NativeJobOptions
        {
            Command = "report.exe",
            Parameters = "--nightly",
            ConsumeStreams = true,
        }));

    q.AddTrigger<NativeJob>(t => t
        .ForJob("nightlyReport")
        .WithCronSchedule("0 0 2 * * ?"));
});
Help us by improving this page!
Last Updated: 10/19/26, 11:45 PM
Contributors: Marko Lahma, Claude Opus 5.5
Next
Serialization (System.Text.Json)