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.
