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
    • Configuration Reference
    • JSON Configuration
    • Cron Expression Reference
    • Multi-Tenancy
    • 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
    • Multiple Triggers
    • Job Template
    • Running Quartz under Aspire
    • Quartz.NET with Wolverine
    • 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

Quartz.Jobs provides some useful ready-made jobs for your convenience.

Quartz provides a number of utility jobs that you can use in your application for doing things like sending e-mails and invoking native processes. These out-of-the-box jobs live in the Quartz.Jobs namespace, which is also the assembly and NuGet package name. In 3.x the namespace was the singular Quartz.Job; a configuration string or a stored JOB_CLASS_NAME naming the old spelling still resolves, with a warning.

Installation

You need to add NuGet package reference to your project which uses Quartz.

dotnet add package Quartz.Jobs

How these jobs are configured

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

Each job also has an options type that maps onto exactly those keys, and an extension that writes it. It is the same stored job either way — but the key cannot be misspelled, the value cannot be of the wrong type, and every setting the job honours is a named property you can find by typing a dot.

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

The extensions work on both configuration surfaces — JobBuilder.Create<TJob>() and the configurator AddJob<TJob>(…) hands you — and each leaves you with what you started with, so the chain continues as usual. Options.FromJobData(map) reads the same settings back out of a job's data.

Features

DirectoryScanJob

Inspects a directory and compares whether any files' "last modified dates" have changed since the last time it was inspected. If one or more files have been updated, created or deleted, the job invokes a call-back method on 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 have been left alone before the job reports it. Without it a file another process is still writing would be handed to the listener half-finished.

The listener is found in one of three ways, 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 name its type, 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. It is not any more, and neither is a same-named type from an assembly you did not mean. Register the listener under 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, rendered with Convert.ToString as the fallback — which for a record or a struct with a compiler-generated ToString is every field it has. So the context is exactly as secret as a job's data map, which is to say not at all: an authorized caller reads both. Put a shared instance there, or a name; keep the connection string and the API key in IConfiguration, a key vault or the container.

Where the directories come from can be decided at run time instead of being listed: implement IDirectoryProvider, put the instance in the SchedulerContext, and name that key as DirectoryProviderName. It is handed the merged job data and returns the paths to scan.

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

FileScanJob

Inspects a single file and compares whether its "last modified date" has changed since the last time it was inspected. If it has, the job invokes a call-back method on 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

When WaitForProcess is on, the integer exit code of the process is saved as the job execution result in the IJobExecutionContext. Turn ConsumeStreams on for a chatty process: one that writes more output than its pipe holds blocks until someone reads it.

Referencing this package changes what an open scheduling endpoint means

Both HTTP surfaces — the HTTP API and the dashboard — schedule a job whose type is a string the request supplies. The name is stored unresolved and resolved later with Type.GetType against whatever is on the host's probing path; there is no allow-list, and the only validation is on the shape of the name. NativeJob is on that path as soon as Quartz.Jobs is referenced, and it starts the executable its job data names with the arguments its job data names. So an unauthenticated Quartz endpoint in a process that references this package is remote code execution rather than an information leak.

You may have this package without a line for it in your project. Quartz.Plugins takes a plain package dependency on Quartz.Jobs, so an application that installed the plugins — for XML scheduling, say — has NativeJob on its probing path with nothing in its own csproj that names Quartz.Jobs. Check your restored graph, not your project file.

Neither surface will start when its mapping says nothing about authorization, which is what closes the common way into this. DirectoryScanJob and FileScanJob read the paths they scan from job data the same way, 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 with the configured content to the configured recipient.

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 route the mail through something other than SmtpClient, or BuildMessage(SendMailOptions) to add to the message — an attachment, a header — before it goes.

This job is an authenticated relay for whoever can schedule it

Sender, Recipient, Subject and Message are all caller data, and so is SmtpHost. Anyone who can schedule a job can therefore send mail claiming to be from any address, to any address, through your server. That is the same trust boundary the rest of this page describes — an authorized caller is trusted — but it is worth naming, because "send mail" reads as harmless in a way that "start a process" does not.

EnableSsl is off by default, which is SmtpClient's own default: turning it on fails outright against a server that does not offer TLS, and a relay on the same host that has been taking this job's mail for years would stop. 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. Job data is durable: a persistent job store writes it to QRTZ_JOB_DETAILS, every node in the cluster reads it, the dashboard shows it, and any export of that table carries it. A password put there is a password in all of those places.

Register the credential with the container instead, bound to the server it belongs to, and the job authenticates with it:

// 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);

CredentialCache is the security choice here, not merely the multi-server convenience. smtp_host is job data, so the host to authenticate to is chosen by whoever scheduled the job; a bare NetworkCredential answers ICredentialsByHost.GetCredential with itself for every host, so pairing the two would hand the registered login to whatever host that job data names — as base64 AUTH LOGIN, to a listener the caller controls. So:

  • a CredentialCache with an entry for the host in job data → that entry is used;
  • a CredentialCache with no entry for it → the mail goes out unauthenticated, rather than authenticating to a stranger;
  • a bare NetworkCredential → the job refuses to send, with a message naming the host and how to bind the credential to it.

Any other ICredentialsByHost of your own is asked GetCredential(host, port, "Basic") and then "login", and its answer is taken as its author's decision.

The password itself belongs wherever the rest of your secrets live — user secrets in development, a key vault or an environment variable in production — and reaches this registration through IConfiguration.

The smtp_username and smtp_password job data keys are still read when nothing is registered, so a job scheduled by an earlier version keeps sending; that path is unaffected by the rule above, because whoever wrote the user name wrote the host beside it. The job logs a warning when it uses them, and a credential from the container wins.

NoOpJob

A job that does nothing. Useful as a placeholder, and for triggering listeners on a schedule without any work attached.

Registering these jobs with the container

The jobs take their dependencies — a TimeProvider, an IServiceProvider, an ICredentialsByHost — from the container, so register them the same way you register your own:

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: 9/9/26, 7:08 PM
Contributors: Marko Lahma, Claude Fable 5.1
Next
Serialization (System.Text.Json)