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

Scheduler Listeners

SchedulerListeners are like ITriggerListeners and IJobListeners, but receive events from the scheduler itself, not necessarily about a specific trigger or job: a job or trigger added or removed, a serious error in the scheduler, the scheduler shutting down, and others.

Caution

Make sure your scheduler listeners never throw an exception (use a try-catch) and that they can handle internal problems. Quartz can get in unpredictable state when it is unable to determine whether required logic in listener was completed successfully when listener notification failed.

The ISchedulerListener Interface

public interface ISchedulerListener
{
    string Name => GetType().Name;

    ValueTask JobScheduled(IScheduler scheduler, ITrigger trigger, CancellationToken cancellationToken = default);

    ValueTask JobUnscheduled(IScheduler scheduler, TriggerKey triggerKey, CancellationToken cancellationToken = default);

    ValueTask TriggerFinalized(IScheduler scheduler, ITrigger trigger, CancellationToken cancellationToken = default);

    ValueTask TriggersPaused(IScheduler scheduler, string? triggerGroup, CancellationToken cancellationToken = default);

    ValueTask TriggersResumed(IScheduler scheduler, string? triggerGroup, CancellationToken cancellationToken = default);

    ValueTask JobsPaused(IScheduler scheduler, string? jobGroup, CancellationToken cancellationToken = default);

    ValueTask JobsResumed(IScheduler scheduler, string? jobGroup, CancellationToken cancellationToken = default);

    ValueTask SchedulerError(IScheduler scheduler, SchedulerErrorContext errorContext, CancellationToken cancellationToken = default);

    ValueTask SchedulerShutdown(IScheduler scheduler, CancellationToken cancellationToken = default);

    // ...and the rest; every member has a do-nothing default implementation, so implement only what you care about
}

A null group in JobsPaused, JobsResumed, TriggersPaused or TriggersResumed means every group.

Every callback names its scheduler

No scheduler-listener callback runs inside a firing, so every member takes the scheduler as its first argument. One listener instance can then serve several schedulers in one host and know which one it is hearing from:

public sealed class AuditSchedulerListener : ISchedulerListener
{
    public ValueTask TriggerPaused(IScheduler scheduler, TriggerKey triggerKey, CancellationToken cancellationToken = default)
    {
        logger.LogInformation("{SchedulerName} paused {TriggerKey}", scheduler.SchedulerName, triggerKey);
        return default;
    }
}

The argument is the scheduler itself, so the listener can act on it: pause the trigger, read Status, ask for the job. For identity only, read SchedulerName and SchedulerInstanceId.

Hearing a reschedule

There is no reschedule callback. RescheduleJob raises two, in this order, once the store has replaced the trigger:

OrderCallbackReceives
1JobUnscheduledthe old trigger's key
2JobScheduledthe new trigger
  • Nothing is raised when the old trigger is not found.
  • A start that re-applies an AddTrigger or ScheduleJob registration to a persistent store reschedules each trigger already stored, so the pair is raised then too. The trigger keeps its run state and its pause (see What a start keeps), and no TriggerPaused is raised for a pause it kept.
  • UpdateTriggerDetails keeps the fire times and raises nothing.
  • Only the scheduler that made the call raises them. Other nodes of a cluster hear nothing.

Reporting an error

SchedulerError is raised when something goes seriously wrong: a job that could not be built, a job store that keeps failing, a job that threw. Its SchedulerErrorContext says what went wrong and, where the scheduler knows, for which trigger, job and firing:

public sealed record SchedulerErrorContext
{
    public required string Message { get; init; }
    public required SchedulerException Exception { get; init; }
    public TriggerKey? TriggerKey { get; init; }
    public JobKey? JobKey { get; init; }
    public string? FireInstanceId { get; init; }
}

The three keys are null when there is nothing to name, such as a scan that never reached a trigger or a store retrying a connection. Every failure inside a firing fills in all three:

public ValueTask SchedulerError(IScheduler scheduler, SchedulerErrorContext errorContext, CancellationToken cancellationToken = default)
{
    if (errorContext.TriggerKey is { } triggerKey)
    {
        return scheduler.PauseTrigger(triggerKey, cancellationToken);
    }

    logger.LogError(errorContext.Exception, "{Message}", errorContext.Message);
    return default;
}

Registering

Any object implementing ISchedulerListener can be registered with the scheduler's ListenerManager.

A scheduler listener is identified by its Name, which defaults to the type's name. Registering a second listener under a taken name replaces the first, so override Name if you register several instances of one type with the same scheduler.

Adding a SchedulerListener:

scheduler.ListenerManager.AddSchedulerListener(mySchedListener);

Removing a SchedulerListener:

scheduler.ListenerManager.RemoveSchedulerListener(mySchedListener.Name);

Register a listener that belongs to the application where the scheduler is configured. The container constructs it, and it is in place before the scheduler starts, so it also hears the starting and started notifications:

builder.AddQuartz(q =>
{
    q.AddSchedulerListener<AuditSchedulerListener>();
});

Overloads take an instance you built or a factory over the service provider, like the ones for job and trigger listeners.

Help us by improving this page!
Last Updated: 10/19/26, 11:45 PM
Contributors: Marko Lahma, Claude Opus 5.5
Prev
Trigger and Job Listeners
Next
Job Execution Middleware