Pixel Pete, the CanaryHub canaryCanaryHub

How to Schedule a Cron Job in Jenkins (and Catch It When It Silently Stops)

How to Schedule a Cron Job in Jenkins (and Catch It When It Silently Stops)

Quick answer

Jenkins schedules jobs with Build Periodically (always builds on a fixed schedule) or Poll SCM (checks for changes on a schedule, builds only if something changed), both using a five-field cron syntax plus Jenkins's own H hash symbol to spread load. If the controller is down when a trigger time passes, that run is skipped for good; Jenkins only catches up on brief scheduler delays while it stays running, not on actual downtime.

Jenkins schedules jobs two built-in ways: Build Periodically, which fires on a fixed schedule no matter what, and Poll SCM, which checks your repository on a schedule and only builds if something changed. Both use the same five-field cron syntax, extended with Jenkins's own H symbol for spreading load across jobs. Neither one tells you when the schedule itself stops firing, which is a different failure than a build that runs and fails.

How do you schedule a cron job in Jenkins?

In a classic Freestyle project, open the job's configuration and check Build periodically (always builds on schedule) or Poll SCM (builds only on a detected change) under Build Triggers, then enter a cron-style spec in the text box.

In a Jenkinsfile, the same two triggers live in a triggers block at the top level of the pipeline:

pipeline {
    agent any
    triggers {
        cron('H 2 * * *')             // builds once a day, sometime around 2am
        // or: pollSCM('H/15 * * * *') // checks for changes roughly every 15 min
    }
    stages {
        stage('Build') {
            steps {
                sh './build.sh'
            }
        }
    }
}

Use cron() when the job should run regardless of whether anything changed: nightly reports, scheduled deploys, cache warmers. Use pollSCM() when a build should only happen after new commits, but you don't have (or want) a webhook wired up from your Git host, since polling checks on a schedule instead of being pushed to.

What's Jenkins's cron syntax, and why doesn't it look like regular cron?

The five fields are the same as standard cron:

MINUTE HOUR DOM MONTH DOW
 0-59  0-23  1-31  1-12 0-7

One detail almost every guide skips: the day-of-week field accepts 0 through 7, and both 0 and 7 mean Sunday. Assume the usual 0-6 range and you'll misread any spec that uses a 7, and the opposite mistake, assuming 7 is invalid, is just as easy to make.

The operators are the usual *, M-N ranges, M-N/X or */X steps, and A,B,C lists. Jenkins adds one more: H, for "hash." Use it instead of a literal number whenever the exact minute doesn't matter:

H H * * *       # once a day, at a stable time chosen per job
H H(0-7) * * *  # once a day, sometime between midnight and 7:59am
H/15 * * * *    # roughly every 15 minutes

H isn't random. It's a hash of the job's name, so the same job always lands on the same minute, but different jobs land on different minutes. Schedule a dozen jobs with 0 0 * * * and they all fire at midnight together, spiking load; schedule them with H H * * * and Jenkins spreads them across the day instead, with no one hand-picking different times. There's one sharp edge: hashes for the day-of-month field are only chosen from 1-28, so a short cycle there like H/3 can leave a 3-6 day gap at the end of longer months.

Jenkins also supports @yearly, @monthly, @weekly, @daily, @midnight, and @hourly as aliases, all hash-balanced the same way. @hourly is H * * * *; @midnight actually means "some time between 12:00 AM and 2:59 AM," not exactly midnight.

Two more things most tutorials skip entirely. You can stack multiple schedules in one trigger, one per line, and they're OR'd together:

H 8 * * *
H(0-30) 17 * * *

That runs the job around 8am and again around 5pm, as two independent lines, not one combined rule. And you can override the timezone by putting a TZ= line first:

TZ=Europe/London
H 8 * * *

Without it, Jenkins schedules in the controller's own system timezone, which is easy to forget if your team isn't in the same one.

Does Jenkins catch up on a cron trigger it missed?

Sometimes. Jenkins's scheduler is a background task that wakes up once a minute and checks the current time against every job's cron spec. If that check runs a little late (the JVM was busy, a GC pause, the thread was briefly starved), Jenkins walks forward minute by minute from where it left off and still fires anything that was due in between. That part looks like a catch-up.

It isn't one if the controller itself was down. The scheduler's internal clock resets to "now" the moment Jenkins restarts, so there's no memory of what should have fired while the process wasn't running at all. A nightly job scheduled for 2am that falls during a maintenance window, a crash-and-restart, or a host reboot simply never fires for that day, and nothing records that it was supposed to. The next run happens at the following day's 2am, right on schedule, which makes it easy to assume nothing was ever missed.

One more detail worth knowing: a triggered build doesn't disappear if no agent is free. cron and pollSCM both just add the job to Jenkins's build queue, where it waits for an executor instead of being dropped. The failure mode that actually loses a run isn't "no agent available," it's "the scheduler's check never happened at all."

How do you find out if a scheduled Jenkins job stopped firing?

Jenkins can tell you a build failed. It has no page that tells you a build didn't start at all on a schedule you expected. In practice, the runs that go missing come from ordinary causes: the controller was down over a weekend, someone disabled the job during a cleanup and never re-enabled it, an agent went permanently offline so the queue just grows, or a Jenkinsfile edit accidentally dropped the triggers block.

The fix is the same one that works for any scheduled task with a silent-failure mode: ping something external after the job succeeds, and let that something alert you if the ping doesn't show up on schedule.

pipeline {
    agent any
    triggers {
        cron('H 2 * * *')
    }
    stages {
        stage('Nightly sync') {
            steps {
                sh './nightly-sync.sh'
            }
        }
    }
    post {
        success {
            sh 'curl -fsS -m 10 --retry 3 "https://ingest.canaryhub.ai/ping/YOUR_TOKEN" > /dev/null'
        }
    }
}

Putting the ping in post { success { ... } } means it only fires when the stage actually completed without error, so a flatline on the monitor's side means either the build failed or it never started at all, both worth knowing about. Set the monitor's expected interval to match the cron schedule plus a grace period a bit above the job's normal runtime. CanaryHub's heartbeat monitors treat the first overdue ping past that grace window as down, not the second, since the grace period is already the buffer.

This is a different signal than Jenkins' own build-failure notifications, deliberately. A failed build tells you something ran and broke. A missing heartbeat tells you nothing ran at all, which is the case a tool watching Jenkins from the inside can't see if Jenkins itself is the thing that's down.

Frequently asked questions

What's the difference between Build Periodically and Poll SCM in Jenkins?
Build Periodically always runs the job on the schedule you set, whether or not anything changed. Poll SCM checks the repository on that schedule and only starts a build if it finds new commits. Reach for Poll SCM only when you don't have a webhook from your Git host wired up; a push-triggered webhook is faster and cheaper than polling.
What does the H symbol mean in a Jenkins cron schedule?
H stands for hash. Jenkins replaces it with a value derived from the job's name, chosen once and stable after that, so jobs scheduled with H H * * * spread across the day instead of all firing at the same minute the way a literal 0 0 * * * would.
Can I set a timezone for a Jenkins cron schedule?
Yes. Put TZ=<zone id>, for example TZ=America/New_York, as the first line of the schedule, before any cron lines. Without it, the schedule runs in the Jenkins controller's own system timezone.
Does Jenkins retry a cron trigger it missed while it was down?
No. Jenkins only catches up on brief delays inside its own scheduler thread while the process keeps running. If the controller was offline when a trigger time passed, that run is skipped permanently, and the next one fires at its normal scheduled time.