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

Trigger and Job Listeners

Listeners are objects you create to act on events in the scheduler. Most applications do not need them; use one when the application must hear about events without the job notifying it.

ListenerEvents
ITriggerListenertrigger firings, misfires, and completions (the job the trigger fired has finished)
IJobListenera job is about to run, was vetoed, reported progress, or has finished

Caution

Make sure your trigger and job listeners never throw an exception (use a try-catch) and that they can handle internal problems. A throwing listener costs the firing:

  • one that throws before the job runs abandons the firing, so the job does not run;
  • one that throws after cannot undo it: the job has run and the trigger has decided what it wants done.

Either way the failure is reported to the scheduler listeners through ISchedulerListener.SchedulerError, wrapped in a JobExecutionProcessException that names the listener and the firing. The trigger is released, including the siblings a [DisallowConcurrentExecution] job was blocking. Nothing gets stuck; the firing is 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 will do with the trigger, from NoInstruction through SetTriggerComplete to DeleteTrigger.
  • Every callback leads with the trigger. Three of them run inside a firing and reach the scheduler through context.Scheduler. TriggerMisfired has no execution, so it takes the scheduler directly:
public ValueTask TriggerMisfired(ITrigger trigger, IScheduler scheduler, CancellationToken cancellationToken = default)
{
    logger.LogWarning("{SchedulerName} missed {TriggerKey}", scheduler.SchedulerName, trigger.Key);
    return default;
}

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);

    ValueTask JobProgressChanged(IJobExecutionContext context, FireInstanceProgress progress, CancellationToken cancellationToken = default);
}
  • jobException is null when the job completed without throwing, so check for null before logging it.
  • JobProgressChanged hears context.ReportProgress in this process, at most once a second per firing and only on a change. A listener that throws here is logged and costs the firing nothing. See Hear progress in a listener.

Using Your Own Listeners

Implement ITriggerListener, IJobListener, or both. Listeners are registered with the scheduler at run time under the name their Name property returns.

Every member of both interfaces has a default implementation: notifications do nothing, and Name returns the type's name. Implement only the events you need. Declare Name only when you register several instances of one type with the same scheduler.

Warning

Because of those defaults, a method whose signature does not match the interface's is not a compile error. It implements nothing, and the default runs instead. Quartz refuses such a listener when it is registered, naming the method and the signature it should have.

Listeners are registered with the scheduler's ListenerManager, with a matcher that selects the jobs or triggers the listener hears about.

Tips

Listeners are registered at run time and are NOT stored in the JobStore with the jobs and triggers. They are usually an integration point with your application, so re-register them every time the application runs.

One job:

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

All jobs of a group:

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

All jobs of two groups:

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

All jobs:

scheduler.ListenerManager.AddJobListener(myJobListener, Matchers.AllJobs());
  • No matcher also means every job: AddJobListener(myJobListener).
  • Matchers are given only at registration. To change them, register again under the same name; the new registration replaces the listener and its matchers together.
  • Listeners are notified in registration order, and that order is guaranteed, so one listener can prepare something the next reads. Re-registering under the same name keeps the listener's place. A listener registered after another was removed is notified last.
  • Matchers builds the roots: Matchers.AllJobs(), Matchers.AllTriggers(), Matchers.Key(key), Matchers.Group<JobKey>(StringOperator.StartsWith, "a"), Matchers.Name<JobKey>(…). Any matcher composes with the And, Or and Not extension methods.

Registering listeners with the container

Register a listener that belongs to the application where the scheduler is configured. The container constructs it:

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, so it survives a restart of the host without a startup hook of your own.

Holding on to a running job's context

To keep hold of executions running in this process, use a job listener. IScheduler.QueryFireInstances lists firings across the cluster as FireInstance projections with keys, times and the owning node. It does not include the job instance, the merged job data map, the result or the cancellation handle, because those exist only where the job runs.

A listener gets the context and can keep it for the execution, keyed by IJobExecutionContext.FireInstanceId to match rows from the listing. The migration guide has the code, about thirty lines, under what is running is a listing.

A listener or a middleware?

Listeners only notify. A listener is told a job is about to run and told what it did; the execution happens between the notifications. A listener cannot wrap the call in a scope or a stopwatch, skip it, or catch or translate its exception: the exception is already classified and the trigger's fate decided.

NeedUse
surround the execution: a log scope, a tenant context, a timing, translating a third-party exceptiona job execution middleware
observe it: audit a completion, chain the next job, count failuresa listener, with matchers to choose jobs and triggers (middleware has none)
veto itITriggerListener.VetoJobExecution

A veto is the scheduler's own refusal and raises JobExecutionVetoed. A middleware that skips the job is invisible outside the pipeline.

Help us by improving this page!
Last Updated: 11/2/26, 10:32 AM
Contributors: dependabot[bot]
Prev
Time and TimeProvider
Next
Scheduler Listeners