How Jenkins Triggers Schedule and Execute Builds: Cron-Based Architecture Explained

Jenkins triggers schedule builds using a cron-based engine that parses timing specifications into bitmasks, calculates the next execution instant via CronTabList.next(), and queues jobs through TimerTrigger.run() when the scheduler's one-minute loop detects a match.

The jenkinsci/jenkins repository implements a sophisticated scheduling system that translates user-defined cron expressions into precise build execution times. Understanding how Jenkins triggers schedule and execute builds requires examining the interaction between the ANTLR-based parser, the calendar arithmetic engine, and the core trigger lifecycle.

Architecture of Jenkins Build Scheduling

Jenkins employs a multi-layered architecture to handle timed builds. The system bridges user configuration, temporal calculations, and the build queue through a coordinated sequence of components.

From Configuration to TimerTrigger

When a user configures a cron schedule (e.g., H/15 * * * *) through the UI or Pipeline, Jenkins stores this specification in the job's config.xml. Upon loading, the system instantiates a TimerTrigger object from hudson.triggers.TimerTrigger, which extends the generic Trigger<T> superclass.

The constructor passes the raw spec string to the parent class, which immediately parses it into a CronTabList—a collection of one or more CronTab objects representing individual cron lines. This parsing occurs via CronTabList.create(), which handles the translation from textual representation to executable schedule.

Parsing Cron Specifications with ANTLR

The cron parser relies on ANTLR grammars defined in core/src/main/antlr4/hudson/scheduler/CrontabLexer.g4 and CrontabParser.g4. This parser translates the spec into four bitmasks representing minutes, hours, days, and months, plus a day-of-week mask.

The resulting CronTab object (from hudson.scheduler.CronTab) can answer temporal queries through methods like check(Calendar), which determines if a given instant matches the specification. For load distribution, the parser utilizes the Hash class (from hudson.scheduler.Hash) to convert the H symbol into a deterministic offset based on the job name.

The One-Minute Scheduling Loop

The core scheduler (hudson.scheduler.Cron) executes once per minute via a dedicated Timer thread. During each iteration, the system queries registered triggers for their next firing time using CronTabList.next().

This method leverages CronTab.ceil(Calendar) to compute the nearest future instant matching the cron specification. Conversely, previous() uses floor() to find past matches. When the current time reaches the computed instant, the scheduler invokes Trigger.run().

How TimerTrigger Executes Builds

When the scheduling loop determines a trigger should fire, it calls the run() method implemented in TimerTrigger. This method performs a straightforward enqueue operation:

job.scheduleBuild(0, new TimerTriggerCause());

The TimerTriggerCause (a subclass of hudson.model.Cause) serves a dual purpose: it places the build in the queue and provides UI metadata indicating the build was "Triggered by Timer." The build executor then picks up the queued item and proceeds through the standard pipeline execution.

Calculating Next Run Times with CronTab and CronTabList

The temporal logic resides primarily in hudson.scheduler.CronTabList and its constituent CronTab instances. These classes handle the complex calendar arithmetic required to resolve cron expressions against real-world time.

Key methods include:

  • check(Calendar) – Determines if the provided calendar instant satisfies the cron specification
  • ceil(Calendar) – Finds the next matching instant at or after the given time
  • floor(Calendar) – Finds the previous matching instant at or before the given time

For jobs utilizing the hash symbol (H) to distribute load, the Hash.from(String) method generates consistent offsets based on project names, ensuring that similarly scheduled jobs do not simultaneously overwhelm the system.

Handling Edge Cases and Invalid Dates

The scheduler includes robust validation for impossible date specifications. When CronTabList.create() or CronTab.ceil() encounters a cron expression that cannot be satisfied—such as 0 0 31 2 * (February 31st)—the system throws RareOrImpossibleDateException (from hudson.scheduler.RareOrImpossibleDateException).

This exception prevents the scheduler from entering an infinite loop searching for non-existent dates and alerts administrators to configuration errors.

Practical Examples

Defining a Timer Trigger in a Pipeline Script

The declarative Pipeline syntax abstracts the underlying TimerTrigger creation:

pipeline {
    triggers {
        // Run every 15 minutes, offset by a hash of the job name
        cron('H/15 * * * *')
    }
    stages {
        stage('Build') {
            steps {
                echo 'Running scheduled build...'
            }
        }
    }
}

This creates a TimerTrigger with the spec 'H/15 * * * *', utilizing the Hash class to calculate the offset.

Manually Creating a TimerTrigger from Java

For programmatic job creation, instantiate the trigger directly:

import hudson.triggers.TimerTrigger;
import hudson.model.FreeStyleProject;
import jenkins.model.Jenkins;

FreeStyleProject job = Jenkins.get().createProject(FreeStyleProject.class, "example");
TimerTrigger trigger = new TimerTrigger("H 0 * * *"); // run once a day at a hashed hour
job.addTrigger(trigger);
job.save(); // persists the trigger in config.xml

The constructor stores the spec string; Jenkins later parses it into a CronTabList via CronTabList.create().

Querying the Next Run Time via Groovy Console

Administrators can inspect future execution times using the Script Console:

import hudson.scheduler.CronTabList
import hudson.scheduler.Hash
import hudson.model.Item

Item item = Jenkins.instance.getItem("example")
String spec = item.getTriggers().values().first().getSpec() // assume only TimerTrigger
CronTabList ctl = CronTabList.create(spec, Hash.from(item.getFullName()))
Calendar next = ctl.next()
println "Next run will be at ${next.getTime()}"

CronTabList.next() internally calls CronTab.ceil to compute the upcoming timestamp.

Handling a Rare or Invalid Spec

When validating user input or testing edge cases, catch impossible date exceptions:

try {
    CronTabList ctl = CronTabList.create("0 0 31 2 *", null); // Feb 31 does not exist
    Calendar next = ctl.next(); // throws RareOrImpossibleDateException
} catch (hudson.scheduler.RareOrImpossibleDateException e) {
    println "Cron spec is impossible to satisfy."
}

The parser detects impossible dates during the ceiling calculation and throws the specific exception defined in hudson.scheduler.RareOrImpossibleDateException.

Summary

  • TimerTrigger (from hudson/triggers/TimerTrigger.java) serves as the concrete implementation that queues builds when cron specifications match.
  • CronTabList and CronTab (in hudson/scheduler/) perform the core calendar arithmetic using bitmasks and ANTLR-parsed grammar.
  • The scheduler runs a one-minute loop checking next() times, invoking run() only when the current time reaches the calculated instant.
  • RareOrImpossibleDateException safeguards against unsatisfiable specifications like February 31st.
  • The Hash class enables load distribution by converting the H symbol into job-specific offsets.

Frequently Asked Questions

How does Jenkins handle the H (hash) symbol in cron expressions?

Jenkins converts the H symbol to a numeric value using hudson.scheduler.Hash.from(String), which hashes the job name to produce a consistent offset within the allowed range. This distributes builds across the time window rather than executing them simultaneously, preventing resource contention when multiple jobs share a schedule like H/15 * * * *.

What happens if a cron spec specifies an impossible date like February 31st?

The system throws RareOrImpossibleDateException (from hudson.scheduler.RareOrImpossibleDateException) when CronTab.ceil() or CronTabList.next() detects that the specification cannot be satisfied within a reasonable time window. This prevents the scheduler from hanging while searching for non-existent calendar dates.

How can I programmatically determine when a Jenkins job will run next?

Access the job's triggers, retrieve the spec string, and call CronTabList.create(spec, Hash.from(jobName)) to obtain a CronTabList instance. Invoking next() on this object returns a Calendar representing the next execution time, calculated using the ceil() method across all configured cron tabs.

What is the difference between the Trigger class and TimerTrigger in Jenkins?

hudson.triggers.Trigger is the abstract base class defining the contract for all trigger types, including SCM polling and remote triggers. TimerTrigger is the concrete implementation specifically designed for cron-based scheduling, extending Trigger to add cron spec parsing and the TimerTriggerCause attribution for build queue entries.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →