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

Multiple Triggers

A job can have any number of triggers. The job carries the data every firing shares; each trigger carries the data that firing needs. Quartz merges the two before the job runs, and the trigger's values win where the keys are the same.

Our 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() is what lets the job be registered on its own rather than alongside one trigger. Without it a job is deleted as soon as its last trigger is gone, which for a job with several triggers is rarely what you want.

The same two triggers built at run time, for a job whose customers are 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 that takes no job detail — schedules a trigger against a job that is already stored, which is why the job was added durably first.

Firing once, with data of its own

TriggerJob fires a stored job immediately, with a data map that is merged the same way a trigger's would be. It creates no trigger, so this is the way to run a job on demand rather than the way 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 parses "50" as happily as it returns 50. GetString is not — given a value stored as an int it returns null rather than "50", and TryGetString returns false. So a job that reads its data with GetString has to be given strings, which is worth remembering when the data comes from somewhere loosely typed. On a persistent store with StoreJobDataAsStrings the question does not arise, because everything is stored, and read back, as a string.

Help us by improving this page!
Last Updated: 9/10/26, 8:50 AM
Contributors: Marko Lahma, Claude Opus 5 (1M context)
Prev
Retrying Failed Jobs
Next
Job Template