Quartz.NETQuartz.NET
Home
Features
Discussions
NuGet
GitHub
Home
Features
Discussions
NuGet
GitHub
  • 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
  • Unreleased Releases

    • Quartz 4.x
      • 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 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
      • Tenancy Patterns
      • Database Schema
      • Database Schema Changes
      • Migration Guide
      • Troubleshooting
      • API Documentation
      • How To's
        • One-Off Job
        • Rescheduling Jobs
        • Multiple Triggers
        • Job Template
        • 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

          • 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
  • 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 runs inside your application. You register a scheduler with the application's service container, describe the jobs and triggers it should start with, and let the host start and stop it. This lesson wires up a scheduler that runs one job; the lessons that follow explain each piece of it.

Install the package

dotnet add package Quartz

That is the whole install for a hosted application. Dependency injection and the hosted service are part of the core package — in 3.x they were the separate Quartz.Extensions.DependencyInjection and Quartz.Extensions.Hosting packages.

Write a job

A job is a class that implements IJob:

public sealed class HelloJob : IJob
{
    private readonly ILogger<HelloJob> logger;

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

    public ValueTask Execute(IJobExecutionContext context, CancellationToken cancellationToken = default)
    {
        logger.LogInformation("Hello from {JobKey}", context.JobDetail.Key);
        return default;
    }
}

The job is constructed from the container for every fire, so it can take whatever the rest of your application takes — a logger, a DbContext, a typed HttpClient. The cancellationToken is the same token as context.CancellationToken; pass it on to everything you await, so a shutdown or an Interrupt call actually reaches your work.

Configure the host

using Microsoft.Extensions.Hosting;
using Quartz;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.AddQuartz(q =>
{
    // run HelloJob now, and then every 40 seconds
    q.ScheduleJob<HelloJob>(trigger => trigger
        .WithIdentity("helloTrigger")
        .StartNow()
        .WithSimpleSchedule(x => x
            .WithInterval(TimeSpan.FromSeconds(40))
            .RepeatForever()));
});

builder.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);

IHost host = builder.Build();

// blocks until the host is stopped, and then until the last running job completes
await host.RunAsync();

AddQuartz registers the scheduler and everything it is made of. AddQuartzHostedService starts it when the host starts and shuts it down when the host stops; WaitForJobsToComplete makes shutdown wait for jobs that are still running instead of cancelling them.

Both hang off IHostApplicationBuilder, so the same two lines work in a web application built with WebApplication.CreateBuilder(args). They are also available on IServiceCollection (builder.Services.AddQuartz(…)) when the registration lives in a method that only has the collection.

Describing jobs and triggers

q.ScheduleJob<TJob>(…) is the short form for the common case: one job, one trigger, the job's identity taken from the trigger's. When a job has several triggers, or when the job is registered somewhere other than where its schedule is, name them separately:

builder.AddQuartz(q =>
{
    JobKey jobKey = new("reportJob");

    q.AddJob<ReportJob>(j => j
        .WithIdentity(jobKey)
        .WithDescription("nightly and on-demand sales report"));

    q.AddTrigger<ReportJob>(t => t
        .ForJob(jobKey)
        .WithIdentity("nightly")
        .WithCronSchedule("0 0 2 * * ?"));

    q.AddTrigger<ReportJob>(t => t
        .ForJob(jobKey)
        .WithIdentity("hourly-on-weekdays")
        .WithCronSchedule("0 0 9-17 ? * MON-FRI"));
});

The type argument on AddTrigger<TJob> is the job the trigger fires. It is what lets the trigger's data be named as properties of that job — see More About Jobs & JobDetails. Use AddTrigger<IJob> when the trigger only names its job by key and you do not need that.

"0 0 2 * * ?" is a cron expression: second, minute, hour, day-of-month, month, day-of-week, so that one is "every day at 02:00". The fields and their special characters are in the Cron Expression Reference, and cron is only one of five schedule kinds — the others are in Lesson 2.

Everything registered this way is stored when the scheduler starts. With a persistent job store it is also what the store already holds that matters: registrations replace stored definitions of the same name by default, which is what makes this list the description of the schedule rather than a one-time seed.

Scheduling at run time

The registrations above are declarative: the application describes the schedule it wants, and the scheduler makes the store match on every start. That is the shape to prefer for a schedule that is part of the application.

Not every schedule is known at startup, though. IScheduler is an ordinary service, so inject it and schedule whenever you like:

public sealed class ReportRequests
{
    private readonly IScheduler scheduler;

    public ReportRequests(IScheduler scheduler)
    {
        this.scheduler = scheduler;
    }

    public async ValueTask QueueFor(string customer, CancellationToken cancellationToken)
    {
        IJobDetail job = JobBuilder.Create<ReportJob>()
            .WithIdentity(customer, "reports")
            .UsingJobData("customer", customer)
            .Build();

        ITrigger trigger = TriggerBuilder.Create()
            .WithIdentity(customer, "reports")
            .StartAt(DateTimeOffset.UtcNow.AddMinutes(5))
            .Build();

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

An application with several schedulers registers each under a name, and injects one by that name with [FromKeyedServices("reporting")] IScheduler scheduler — see Multiple schedulers.

The scheduler's lifecycle

  • Triggers do not fire until the scheduler has been started. The hosted service does that for you.
  • Standby() stops firing without shutting anything down; Start() resumes. Jobs already running keep running.
  • Shutdown() is final. A scheduler that has been shut down cannot be started again — build a new one.
  • The scheduler is IAsyncDisposable, and disposing it shuts it down and releases what it owns. Under a host, the host does that.

In Lesson 2 we take a quick tour of jobs and triggers, so that the code above reads as more than an incantation.

Help us by improving this page!
Last Updated: 8/23/26, 6:42 AM
Contributors: Marko Lahma
Next
Jobs And Triggers