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

Trigger and Job Listeners

Listeners are objects that you create to perform actions based on events occurring within the scheduler. As you can probably guess, TriggerListeners receive events related to triggers, and JobListeners receive events related to jobs.

Trigger-related events include: trigger firings, trigger mis-firings (discussed in the "Triggers" section of this document), and trigger completions (the jobs fired off by the trigger is finished).

Caution

Make sure your trigger and job listeners never throw an exception (use a try-catch) and that they can handle internal problems. What a throwing listener costs is the firing: one that throws on the way in abandons it, so the job does not run, and one that throws on the way out cannot undo it, because the job has already run and the trigger has already decided what it wants done. In both cases the failure is reported to the scheduler listeners through ISchedulerListener.SchedulerError, wrapped in a JobExecutionProcessException that names the listener and the firing, and the trigger is released either way — including the siblings a [DisallowConcurrentExecution] job was blocking. Nothing gets stuck; the firing is simply lost.

The ITriggerListener Interface

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

    ValueTask TriggerFired(ITrigger trigger, IJobExecutionContext context, CancellationToken cancellationToken = default);

    ValueTask<bool> VetoJobExecution(ITrigger trigger, IJobExecutionContext context, CancellationToken cancellationToken = default);

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

    ValueTask TriggerComplete(ITrigger trigger, IJobExecutionContext context, SchedulerInstruction triggerInstructionCode, CancellationToken cancellationToken = default);
}

triggerInstructionCode is the SchedulerInstruction the trigger returned for this fire — what the scheduler is about to do with the trigger, from NoInstruction through SetTriggerComplete to DeleteTrigger.

Every callback leads with the trigger it is about. A listener reaches the scheduler it serves through its execution context, or as a second argument when there is no execution. Three of these four callbacks happen inside a firing, so they read context.Scheduler; TriggerMisfired is the exception, because a misfire is noticed rather than executed, and it takes the scheduler directly in the place the context has:

public ValueTask TriggerMisfired(ITrigger trigger, IScheduler scheduler, CancellationToken cancellationToken = default)
{
    logger.LogWarning("{SchedulerName} missed {TriggerKey}", scheduler.SchedulerName, trigger.Key);
    return default;
}

Job-related events include: a notification that the job is about to be executed, and a notification when the job has completed execution.

The IJobListener Interface

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

    ValueTask JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = default);

    ValueTask JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = default);

    ValueTask JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, CancellationToken cancellationToken = default);
}

jobException is null when the job completed without throwing, so a listener that only reacts to failures starts with a null check rather than assuming there is an exception to log.

Using Your Own Listeners

To create a listener, simply create an object the implements either the ITriggerListener and/or IJobListener interface. Listeners are then registered with the scheduler during run time under a name, which their Name property advertises.

Every member of both interfaces has a default implementation — the notifications do nothing, and Name returns the type's name — so implement only the events you're interested in, and only declare Name when you register several instances of one type with the same scheduler.

Warning

The price of those defaults is that a method whose signature does not match the interface's is not a compile error. It simply stops implementing anything, and the default runs in its place — the method is never called. Quartz refuses a listener in that shape when it is registered, naming the method and the signature it should have, rather than attaching one that will be silent.

Listeners are registered with the scheduler's ListenerManager along with a Matcher that describes which Jobs/Triggers the listener wants to receive events for.

Tips

Listeners are registered with the scheduler during run time, and are NOT stored in the JobStore along with the jobs and triggers. This is because listeners are typically an integration point with your application. Hence, each time your application runs, the listeners need to be re-registered with the scheduler.

Adding a JobListener that is interested in a particular job:

scheduler.ListenerManager.AddJobListener(myJobListener, Matchers.Key(new JobKey("myJobName", "myJobGroup")));

Adding a JobListener that is interested in all jobs of a particular group:

scheduler.ListenerManager.AddJobListener(myJobListener, GroupMatcher<JobKey>.GroupEquals("myJobGroup"));

Adding a JobListener that is interested in all jobs of two particular groups:

scheduler.ListenerManager.AddJobListener(myJobListener,
    GroupMatcher<JobKey>.GroupEquals("myJobGroup").Or(GroupMatcher<JobKey>.GroupEquals("yourGroup")));

Adding a JobListener that is interested in all jobs:

scheduler.ListenerManager.AddJobListener(myJobListener, Matchers.AllJobs());

Passing no matcher at all means the same thing — a listener with no matchers hears about every job — so AddJobListener(myJobListener) is the shortest way to say it.

Registration is the only moment matchers are given. A listener that has to start hearing about something else is registered again under the same name, with the matchers it needs: the second registration replaces the listener and its matchers together, so the two can never be out of step.

Listeners are notified in the order they were registered, and this is a promise rather than an accident of the implementation — so two listeners that are not independent, such as one that prepares something the next one reads, can be built on it. Registering again under the same name replaces a listener where it stands, and one registered after another was removed is notified last rather than in the removed one's place.

The Matchers class is the entry point: its static factories build the roots (Matchers.AllJobs(), Matchers.AllTriggers(), Matchers.Key(key), Matchers.Group<JobKey>(StringOperator.StartsWith, "a"), Matchers.Name<JobKey>(…)), and any matcher composes with the And, Or and Not extension methods.

Registering listeners with the container

A listener that belongs to the application rather than to a moment in its run is registered where the scheduler is configured, and constructed from the container like anything else:

builder.AddQuartz(q =>
{
    // every job
    q.AddJobListener<AuditListener>();

    // only the reporting group, and only triggers whose name starts with "nightly"
    q.AddJobListener<ReportAuditListener>(GroupMatcher<JobKey>.GroupEquals("reports"));
    q.AddTriggerListener<NightlyListener>(NameMatcher<TriggerKey>.NameStartsWith("nightly"));

    // an instance you built yourself, or a factory over the provider
    q.AddTriggerListener(new VetoWeekends(), Matchers.AllTriggers());
    q.AddJobListener(provider => new MeteredListener(provider.GetRequiredService<IMeterFactory>()));
});

This is the same registration the ListenerManager calls perform, done before the scheduler starts, which is what makes it survive a restart of the host without a startup hook of your own.

Holding on to a running job's context

A job listener is also the way to keep hold of the executions running in this process. The scheduler does not hand them out: IScheduler.QueryFireInstances lists firings across the cluster as FireInstance projections, which carry keys, times and the owning node — but not the job instance, the merged job data map, the result or the cancellation handle, because those exist only where the job is running.

A listener is handed the context and can keep it for the duration of the execution, keyed by IJobExecutionContext.FireInstanceId so that a row the listing returned and a context you are holding can be matched up. The migration guide has the whole thing in about thirty lines, under what is running is a listing.

A listener or a middleware?

Listeners are notification-only, and that is the whole distinction. A listener is told that a job is about to run and told what it did; the execution happens between the two notifications rather than inside them. So a listener cannot wrap the call in a scope or a stopwatch, cannot decline to make it, and cannot catch or translate what it threw — the exception it is handed has already been classified, and the trigger's fate has already been decided.

Code that needs to surround the execution is a job execution middleware: a log scope, a tenant context, a timing, a translation of what a third-party library throws. Code that needs to observe one — audit a completion, chain the next job, count failures — is a listener, and gets matchers to choose which jobs and triggers it hears about, which middleware has no equivalent of. Vetoing stays a listener's job: ITriggerListener.VetoJobExecution is the scheduler's own refusal, and it raises JobExecutionVetoed, whereas a middleware that declines to run the job is invisible from outside the pipeline.

Listeners are not used by most users of Quartz.NET, but are handy when application requirements create the need for the notification of events, without the Job itself explicitly notifying the application.

Help us by improving this page!
Last Updated: 9/15/26, 4:15 PM
Contributors: Marko Lahma, Claude Opus 5 (1M context)
Prev
Time and TimeProvider
Next
Scheduler Listeners