Triggers & webhooks

Build on push, on schedule, on demand.

Beginner10 min · lesson 8 of 16

You push a one-line fix, tab over to Slack, and start typing 'deploying now'. On a well-wired Jenkins the build is already running before you finish the sentence. On a badly-wired one you sit refreshing the job page for four minutes, wondering whether anything even noticed. The difference is the trigger: the rule that decides when a pipeline starts. It works like the doorbell wiring on a house. Jenkins never runs a job on a hunch, so something has to press the button. This lesson covers the three automated ways to press it, all of them declared in code: a push notification (a webhook), a wall-clock schedule (cron), and a periodic check (polling). It also covers the humans and upstream jobs that round them out, and exactly how a webhook-driven build fires the instant a commit lands.

Three automatic ways to start a build, plus two by hand

A trigger is a condition you configure that puts a build in the queue. A webhook is the fastest one and the one you should reach for first. It behaves like the bell over a shop door: the moment someone walks in, you hear it. Technically it is an inbound HTTP (HyperText Transfer Protocol, the language web servers speak) request that your source-control manager (the SCM, the server holding your git repository, such as GitHub, GitLab, or Bitbucket) sends to Jenkins the instant a push lands. Polling is the mirror image, and it behaves like walking to the front door every fifteen minutes to check whether anybody came by. Jenkins asks the SCM 'anything new since last time?' on a fixed schedule. That is the fallback for a Jenkins sitting behind a firewall that cannot receive inbound calls. It burns API (application programming interface) calls and adds up to a full interval of lag. cron fires on the wall clock whether or not anyone committed anything: nightly image rebuilds, weekly cache cleanups. And a build can always be started by hand from the web UI (user interface) or the CLI (command-line interface), or by an upstream job finishing. All three automated kinds live in one place in a declarative pipeline, the triggers block, so the timing rules get versioned and code-reviewed right beside the steps they launch.

A webhook-driven build, end to end
1git push
developer pushes a commit to main
2GitHub POST
signed JSON payload sent to /github-webhook/
3Verify HMAC
Jenkins checks the signature against the shared secret
4Match & queue
plugin finds jobs on that repo and queues a build
5Agent runs
cause is 'Started by GitHub push'; stages execute
No timer sits anywhere in this path. The build is queued milliseconds after the push, and any POST with a missing or wrong signature is dropped at the verify step.

Under the hood, one controller thread wakes about once a minute and walks every job's cron and pollSCM schedule to see what is due. The controller is the Jenkins brain that decides what to queue, while agents are the worker machines that actually run the builds. Webhooks skip that timer completely. They arrive as HTTP POSTs and put a build straight into the queue. There is one catch that catches everybody exactly once. The triggers block lives inside the Jenkinsfile, so Jenkins learns your schedule only by reading that file, and it reads that file only when the job runs. On a brand-new job the triggers sit there doing nothing until the first manual build registers them.

Reading a cron line, and why you should write H

A Jenkins cron string is five fields separated by spaces: minute (0-59), hour (0-23), day of month (1-31), month (1-12), and day of week (0-7, where both 0 and 7 mean Sunday). A * means 'every'. So 0 2 * * * reads as '2:00 AM, every day'. Hardcoding that minute becomes a trap once you have a few dozen jobs. If fifty nightly builds all say minute 0, they go off together at 02:00 like fifty alarm clocks in one room, and the agents choke. The H symbol fixes that. H stands for 'hash'. cron('H 2 * * *') still runs once inside the 2 AM hour, but at a minute Jenkins works out by hashing the job's name, so those fifty jobs scatter across 02:00 to 02:59 and each one keeps the same minute from run to run. Use H anywhere you would otherwise pin a number. H/15 * * * * means 'every 15 minutes, offset by the hash' rather than exactly on the quarter hour. Aliases exist too, @daily, @midnight and @weekly, and Jenkins prints the next few fire times right under the field so you can sanity-check what you typed.

Jenkinsfile
pipeline {
agent any
triggers {
githubPush() // build on a GitHub webhook (needs the GitHub plugin)
cron('H 2 * * *') // nightly, hashed minute inside the 02:00 hour
pollSCM('H/15 * * * *') // fallback ONLY: poll git ~every 15 min where hooks can't reach
}
stages {
stage('Build') {
steps {
sh 'make build'
}
}
}
}
Console Output — build #47 (nightly run)
Started by timer
Obtained Jenkinsfile from git https://github.com/acme/payments-api.git
[Pipeline] Start of Pipeline
[Pipeline] node
Running on built-in in /var/jenkins_home/workspace/payments-api
[Pipeline] stage
[Pipeline] { (Build)
[Pipeline] sh
+ make build
gcc -O2 -o app main.c
[Pipeline] }
[Pipeline] End of Pipeline
Finished: SUCCESS

'Started by timer' at the top of a console log is Jenkins naming the cause, which is its word for whichever trigger fired this run. A pollSCM run that found a change prints 'Started by an SCM change' instead. If polling finds nothing, no build appears at all, which is the whole point of it. Keep pollSCM as the fallback for firewalled controllers and nothing else. Once a webhook works, delete the poll line, because running both means Jenkins is being poked on every push and still asking the repository on a timer.

Wiring the webhook

For a single pipeline job, githubPush() in the triggers block is what wires it to GitHub. It comes from the GitHub plugin, which you install below. In an older Freestyle job the same switch is a checkbox with the baffling label 'GitHub hook trigger for GITScm polling'. That name is a leftover from history. When the hook arrives Jenkins does still run a lightweight git poll, but only of that one job, and only because the hook nudged it, never on a timer. Underneath, GitHub sends an HTTP POST carrying a JSON (JavaScript Object Notation, a plain-text data format) payload to https://your-jenkins/github-webhook/, and the trailing slash matters. The payload names the repository and the pushed ref, and the plugin matches it against every job whose git remote points at that repository. That endpoint takes no login by design, which is why GitHub signs each delivery with an HMAC (hash-based message authentication code, a fingerprint of the request body computed with a secret only the two ends know). Set that shared secret in both GitHub and Jenkins and Jenkins throws away any POST whose signature does not check out. Skip it and anyone who can reach the URL can queue builds on your controller. A queued build runs real pipeline steps on an agent, with whatever credentials the job binds, so an unverified endpoint is remote code execution sitting there waiting to happen.

shell — confirm the endpoint, then push a real commit
# The webhook receiver answers 200 to a POST (this is where GitHub delivers)
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://jenkins.example.com/github-webhook/
# Make a real change and let GitHub deliver the hook
git commit -am 'fix: null check in charge handler'
git push origin main
stdout + jenkins.log (the hook arriving, the build starting)
200
[main 9f3c1a2] fix: null check in charge handler
To github.com:acme/payments-api.git
3b1e0f4..9f3c1a2 main -> main
# tail -f /var/log/jenkins/jenkins.log
INFO o.j.p.github.webhook.subscriber.DefaultPushGHEventSubscriber#onEvent:
Received PushEvent for https://github.com/acme/payments-api from 140.82.115.1
INFO o.j.p.github.webhook.subscriber.DefaultPushGHEventSubscriber#onEvent:
Poked payments-api
INFO com.cloudbees.jenkins.GitHubPushTrigger#run:
SCM changes detected in payments-api. Triggering #48
# Console Output — build #48
Started by GitHub push by alice
Finished: SUCCESS

Multibranch jobs scan, they do not poll

A multibranch pipeline is one job that reads a whole repository and creates a child pipeline for every branch containing a Jenkinsfile. Feature branches and pull requests then build themselves, with no hand-made job per branch. Multibranch jobs do not use the triggers block for source-control events. The branch source owns that wiring instead. When a webhook arrives, Jenkins re-indexes only the branch that changed and builds it: event-driven, no timer anywhere. The multibranch version of polling is the option called 'Periodically if not otherwise run', a folder-level scan that re-lists branches on a schedule and brings back the same lag and the same API cost. Point an organization folder at your GitHub org and Jenkins will register the webhooks for you through GitHub's API, so a newly created repository is wired the moment it shows up. That is why manageHooks and a stored credential turn up in the config below.

shell — install the GitHub plugins (on the Jenkins host)
jenkins-plugin-cli --plugins \
github:1.40.1 \
github-branch-source:1811.v9d194e77c709 \
workflow-multibranch:800.v90f7ac4e6c8b
stdout
Loading plugins from --plugins argument
Downloading github:1.40.1
Downloading github-branch-source:1811.v9d194e77c709
Downloading workflow-multibranch:800.v90f7ac4e6c8b
Done
jenkins.yaml (Configuration as Code)
unclassified:
gitHubPluginConfig:
hookUrl: "https://jenkins.example.com/github-webhook/"
hookSecretConfigs:
- credentialsId: "github-webhook-secret" # HMAC secret used to verify each delivery
configs:
- name: "acme"
apiUrl: "https://api.github.com"
credentialsId: "github-app-creds"
manageHooks: true # let Jenkins register the webhook itself
shell — apply the config and confirm exit 0
java -jar jenkins-cli.jar -s https://jenkins.example.com/ -auth @/root/.jenkins-cli reload-jcasc-configuration
echo $?
stdout
0
A new trigger stays asleep until the job runs once
The triggers block lives inside the Jenkinsfile, and the only way Jenkins learns what is in that file is by running the job. So on a brand-new pipeline your cron and githubPush triggers sit there silently doing nothing until you kick off the first build by hand, or press 'Scan Repository Now' on a multibranch job. Teams lose whole afternoons to 'my nightly build never fired' when the fix is one manual run to register the schedule. The same trap bites when you change an existing schedule: the new value only takes hold on the run after Jenkins next reads the file.
Quick check
01You add triggers { cron('H 2 * * *') } to a brand-new pipeline job, save it, and go home. Next morning, nothing ran overnight. What is most likely going on?
Incorrect — No. H is valid in every field, minute included, and spreading load across the minute is exactly what it is for. The string is fine.
Correct — A declarative trigger only registers after Jenkins reads the Jenkinsfile, and that happens on the first build. One manual run arms the schedule.
Incorrect — No. The fields are minute, hour, day of month, month, day of week. The 2 is the hour and day of month is *, so it runs every day.
Incorrect — No. cron and pollSCM are built into the pipeline engine. Only githubPush() needs the GitHub plugin.
02The /github-webhook/ endpoint takes no login at all. So what actually stops a stranger who can reach that URL from queueing builds on your Jenkins?
Incorrect — No. The endpoint is unauthenticated, so there is no token check on the delivery itself.
Correct — Set the shared secret in both GitHub and Jenkins and forged deliveries get dropped. Skip it and the open endpoint turns into remote code execution waiting to happen.
Incorrect — No. What authenticates a delivery here is the HMAC signature, not an IP allow-list.
Incorrect — No. A queued build runs real steps on an agent with whatever credentials the job binds, which is precisely why an unverified endpoint is dangerous.
03A run's console log opens with 'Started by an SCM change'. The triggers block holds githubPush(), cron('H 2 * * *'), and pollSCM('H/15 * * * *'). Which one most likely started this build?
Incorrect — No. A cron-started run logs 'Started by timer', not 'Started by an SCM change'.
Incorrect — No. A manual run is logged as started by that user, not as an SCM (source-control manager) change.
Correct — 'Started by an SCM change' is the cause pollSCM writes when it finds a diff. A webhook would log 'Started by GitHub push' instead.
Incorrect — No. A webhook-triggered build logs 'Started by GitHub push', not 'Started by an SCM change'.

Your pipeline now starts when it should: on every push, on a nightly schedule that does not stampede, and by hand when you want it. Next up is Build & test stages, which is about what a run should actually do once one of these triggers fires. Compile the code, run the test suite, and fail fast so a bad commit never reaches the stages that come after.

Try this

Work through “Multibranch jobs scan, they do not poll” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: a new trigger stays asleep until the job runs once. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related