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

Multiple Triggers

A job can have any number of triggers. Put the data every firing shares on the job, and each firing's own data on its trigger. Quartz merges the two before the job runs; on a shared key the trigger's value wins.

The example job reads both:

public sealed class CustomerProcessJob : IJob
{
    public static readonly JobKey Key = new("customer-process", "batch");

    private readonly ILogger<CustomerProcessJob> logger;

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

    public ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        JobDataMap data = context.MergedJobDataMap;

        string? customerId = data.GetString("CustomerId");
        int batchSize = data.GetInt("batch-size");

        logger.LogInformation("CustomerId={CustomerId} batch-size={BatchSize}", customerId, batchSize);
        return default;
    }
}

One job, two triggers

Register the job once and give it two triggers, each with its own data:

builder.Services.AddQuartz(q =>
{
    q.AddJob<CustomerProcessJob>(j => j
        .WithIdentity(CustomerProcessJob.Key)
        .StoreDurably()
        .UsingJobData("batch-size", 50));

    q.AddTrigger<CustomerProcessJob>(t => t
        .ForJob(CustomerProcessJob.Key)
        .WithIdentity("customer-1-hourly")
        .UsingJobData("CustomerId", "1")
        .WithCronSchedule("0 0 * ? * *"));

    q.AddTrigger<CustomerProcessJob>(t => t
        .ForJob(CustomerProcessJob.Key)
        .WithIdentity("customer-2-nightly")
        .UsingJobData("CustomerId", "2")
        .UsingJobData("batch-size", 500)   // this trigger overrides the job's value
        .WithCronSchedule("0 0 2 ? * *"));
});

The hourly firing logs CustomerId=1 batch-size=50; the nightly one logs CustomerId=2 batch-size=500.

StoreDurably() lets the job be registered on its own rather than alongside one trigger. Without it the job is deleted as soon as its last trigger is gone.

The same triggers built at run time, for customers not known at startup:

public async ValueTask ScheduleFor(
    IScheduler scheduler,
    IReadOnlyCollection<string> customers,
    CancellationToken cancellationToken)
{
    IJobDetail job = JobBuilder.Create<CustomerProcessJob>()
        .WithIdentity(CustomerProcessJob.Key)
        .StoreDurably()
        .UsingJobData("batch-size", 50)
        .Build();

    await scheduler.AddJob(job, new AddJobOptions { Replace = true }, cancellationToken);

    foreach (string customer in customers)
    {
        ITrigger trigger = TriggerBuilder.Create()
            .WithIdentity($"customer-{customer}", "batch")
            .ForJob(CustomerProcessJob.Key)
            .UsingJobData("CustomerId", customer)
            .WithCronSchedule("0 0 * ? * *")
            .Build();

        await scheduler.ScheduleJob(trigger, cancellationToken: cancellationToken);
    }
}

ScheduleJob(trigger), the overload without a job detail, schedules a trigger against a job that is already stored. That is why the job is added durably first.

Once each time the scheduler starts

From 4.4. RunAtStartup(jobKey) runs a job once whenever the scheduler starts, beside its own triggers. It is what a crontab's @reboot means.

builder.Services.AddQuartz(q =>
{
    q.ScheduleJob<CustomerProcessJob>(
        trigger => trigger.WithIdentity("customer-process-hourly").WithCronSchedule("0 0 * ? * *"),
        job => job.WithIdentity(CustomerProcessJob.Key));

    // And once each time the scheduler starts, so the first run does not wait for the hour.
    q.RunAtStartup(CustomerProcessJob.Key);
});
WhenRuns
The scheduler startsonce, as soon as it can
It is built in standby, and started lateronce, at that start
It leaves standbyno
The application restartsonce again
Each node of a cluster startsonce on that node
  • The run is a one-shot trigger in SchedulerConstants.StartupGroup (QRTZ_STARTUP), named anew on each start and deleted once it has fired. context.Trigger.Key.Group tells a startup run from a scheduled one.
  • It is an ordinary trigger: [DisallowConcurrentExecution], listeners and history apply. From 4.6 it carries its job's execution group, so limits and routing apply too.
  • On a persistent store it is stored until it has fired. A run that a crash interrupts is recovered like any other firing.
  • A run that a crash prevented is replaced, not added to. The next start of the same node unschedules that node's unfired startup triggers for the job. In a cluster it leaves other nodes' alone, and one already reserved or running is left to finish.
  • In a cluster it is pinned to its node with PreferredNode.For. It fails over to another node only while that node is down. A node that refuses the job's execution group leaves its run waiting, as for any pinned trigger.
  • The job has to be stored by then, durably or with a trigger of its own. If it is not, the start fails with SchedulerException.
  • Naming the same job twice registers one run.

Firing once, with data of its own

TriggerJob fires a stored job immediately, with a data map merged the same way a trigger's is. It creates no trigger: use it to run a job on demand, not to schedule it.

JobDataMap data = new() { { "CustomerId", "3" }, { "batch-size", 10 } };
await scheduler.TriggerJob(CustomerProcessJob.Key, data, cancellationToken);

GetString is the strict one

The numeric accessors are forgiving: GetInt returns 50 for both 50 and "50". GetString is not: for a value stored as an int it returns null rather than "50", and TryGetString returns false. A job that reads with GetString must be given strings. On a persistent store with StoreJobDataAsStrings this cannot happen, because everything is stored and read back as a string.

Help us by improving this page!
Last Updated: 10/26/26, 7:29 AM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
Job Outcomes
Next
Job Template