Jenkins Env Vars: The Complete Reference (and Pitfalls)
Master Jenkins environment variables: BUILD_ID, JOB_NAME, NODE_NAME, WORKSPACE, JENKINS_HOME.
20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.
- ✓Solid grasp of devops fundamentals
- ✓Comfortable reading and writing code examples independently
- ✓Basic understanding of production deployment concepts
- Use
env.BUILD_IDin Declarative,env.BUILD_IDorwithEnvin Scripted — avoid$BUILD_IDin shell steps in Declarative. BUILD_IDis deprecated since Jenkins 2.0; useBUILD_NUMBERfor the build number.WORKSPACEon agents points to the agent's workspace, not the master — always useenv.WORKSPACEto be safe.- Stage-level
environment {}blocks override pipeline-level variables for that stage only. withEnv(['VAR=value'])in Scripted Pipeline creates a scoped environment block.- Credentials in
environment {}viacredentials('id')map to_USRand_PSWvariables. - Dot notation (
env.SOME_VAR) is safer than string interpolation inshsteps. JENKINS_HOMEis the master's home directory, not available on agents by default.
Imagine Jenkins is a factory floor with many workstations (agents). Each workstation has a whiteboard (environment) where the foreman writes important info like job name, build number, and workspace path. The foreman uses a special marker (environment variables) that workers (pipeline steps) can read. Some info is written in permanent marker (pipeline-level env vars), some in dry-erase (stage-level) that gets wiped between stages. If a worker writes on the whiteboard with a different marker (withEnv), it only affects that worker's task. The factory also has a secure locker (credentials) where passwords are stored; the foreman can copy them to the whiteboard as _USR and _PSW for workers to use. But beware: the foreman sometimes uses an old marker (BUILD_ID) that writes numbers that don't match the actual build number — that marker is being retired.
I still remember the Friday afternoon when a production deployment failed because a shell script used $BUILD_ID to tag a Docker image. The tag was '1' instead of '45', overwriting our production image. We spent hours debugging why the wrong image was deployed. That was my painful introduction to Jenkins environment variable nuances. Jenkins has been around since 2011, and its environment variable system carries legacy baggage. Variables like BUILD_ID were deprecated in Jenkins 2.0 but still exist for backward compatibility, causing silent data corruption. This article is the reference I wish I had back then: a complete guide to every built-in variable, their scoping rules, and the gotchas that bite you in production. We'll cover the full list: BUILD_ID, JOB_NAME, NODE_NAME, BUILD_URL, WORKSPACE, JENKINS_HOME, and more. You'll learn the difference between dot notation and env. notation in Declarative Pipelines, how to scope variables to stages, use withEnv in Scripted Pipelines, dynamically generate variables, map credentials, and avoid common pitfalls like BUILD_ID deprecation and WORKSPACE agent issues. By the end, you'll never be caught off guard by a misbehaving environment variable again.
Complete Built-in Environment Variable Reference
Jenkins provides a set of built-in environment variables that are automatically set for every build. Here is the complete list with descriptions and typical values:
BUILD_ID: Deprecated, useBUILD_NUMBER. Example:1(but may be inconsistent).BUILD_NUMBER: The current build number, e.g.,45.BUILD_TAG: String ofjenkins-${JOB_NAME}-${BUILD_NUMBER}, e.g.,jenkins-my-job-45.BUILD_URL: Full URL of the build, e.g.,http://jenkins.example.com/job/my-job/45/.EXECUTOR_NUMBER: Number of the executor on the node, e.g.,0.JAVA_HOME: Java home path, if set.JENKINS_HOME: Jenkins master's home directory, e.g.,/var/jenkins_home. Not set on agents by default.JENKINS_URL: Jenkins master URL, e.g.,http://jenkins.example.com/.JOB_BASE_NAME: Short job name without folder path, e.g.,my-job.JOB_DISPLAY_NAME: Display name of the job.JOB_NAME: Full job name with folders, e.g.,folder/my-job.JOB_URL: Full URL of the job.NODE_NAME: Name of the node (agent) running the build, e.g.,agent-1.NODE_LABELS: Space-separated list of labels on the node.WORKSPACE: Path to the workspace on the node, e.g.,/home/jenkins/workspace/my-job.
Production Insight: In a multi-branch pipeline, JOB_NAME includes the branch name, e.g., my-job/feature%2Fbranch. Always use env.JOB_NAME in pipeline steps to get the correct value. We once had a script that used $JOB_NAME and failed on branches with slashes because the slash was URL-encoded.
BUILD_ID was deprecated in Jenkins 2.0. It may return the build number as a string, but can be inconsistent across plugins. Always use BUILD_NUMBER for numeric build ID.$BUILD_ID to name artifacts. After a Jenkins upgrade, BUILD_ID started returning '1' for all builds, causing artifact overwrites. Switched to $BUILD_NUMBER and added a validation step.BUILD_ID.Dot Notation vs env. Notation in Declarative Pipelines
In Declarative Pipeline, you can access environment variables using dot notation (env.BUILD_NUMBER) or directly as environment variables ($BUILD_NUMBER). However, there are critical differences:
- Dot notation (
env.BUILD_NUMBER) is Groovy code that evaluates at the pipeline execution context. It is safe inshsteps when used with string interpolation:sh 'echo ${env.BUILD_NUMBER}'. But avoidsh 'echo $BUILD_NUMBER'because it relies on shell environment, which may not be set in all contexts. - env. notation is not valid; you cannot write
env.BUILD_NUMBERin a shell step without$prefix. - In
environment {}blocks, you must use theenvprefix:environment { MY_VAR = env.BUILD_NUMBER }is wrong; useenvironment { MY_VAR = "${env.BUILD_NUMBER}" }orenvironment { MY_VAR = BUILD_NUMBER }.
Best Practice: Always use env.VARIABLE in Groovy expressions, and $VARIABLE in shell steps within sh scripts. For safety, use triple-double-quoted strings with interpolation: sh """echo ${env.BUILD_NUMBER}""".
Example: ``groovy pipeline { agent any environment { MY_TAG = "${env.BUILD_NUMBER}-${env.BRANCH_NAME}" } stages { stage('Build') { steps { sh """ echo "Tag: ${env.MY_TAG}" docker build -t myapp:${env.MY_TAG} . """ } } } } ``
environment { MY_VAR = "${env.OTHER_VAR}" } to ensure correct evaluation.environment { MY_VAR = "$BUILD_NUMBER" } set MY_VAR to literal '$BUILD_NUMBER' because the shell expanded it before Groovy. Fixed by using "${env.BUILD_NUMBER}".env.VAR in Groovy, $VAR in shell. Never mix without understanding expansion order.Stage-Level Environment Variable Scoping
In Declarative Pipeline, you can define environment variables at the pipeline level (global) or at the stage level. Stage-level variables override pipeline-level variables for that stage only. They are not available in subsequent stages.
Example: ``groovy pipeline { agent any environment { DEPLOY_ENV = 'production' } stages { stage('Build') { environment { DEPLOY_ENV = 'staging' // overrides for this stage } steps { sh 'echo $DEPLOY_ENV' // prints 'staging' } } stage('Deploy') { steps { sh 'echo $DEPLOY_ENV' // prints 'production' (pipeline-level) } } } } ``
- Pipeline-level
environment {}block sets variables for all stages. - Stage-level
environment {}block overrides for that stage. - Variables set with
env.VAR = 'value'in ascriptblock are global and persist across stages. withEnvin Scripted Pipeline creates a temporary scope; variables are not available after the block.
Production Insight: We had a stage that set DATABASE_URL to a test database, but the next stage still used the production one because we assumed stage-level variables were global. We moved the variable to pipeline-level and used a condition to set it differently per stage.
NODE_ENV = 'test' in a stage-level environment block and expected it to persist across all stages. After the stage, NODE_ENV reverted to pipeline-level value, causing test failures. Moved to script { env.NODE_ENV = 'test' } for global effect.script { env.VAR = 'value' } for global changes.withEnv Step Usage for Scripted Pipelines
In Scripted Pipeline, withEnv is the primary way to set environment variables temporarily. It takes a list of 'KEY=value' strings and executes a closure with those variables set. Variables are restored after the block.
Syntax: ``groovy withEnv(['MY_VAR=hello', 'ANOTHER_VAR=world']) { sh 'echo $MY_VAR' // prints 'hello' } sh 'echo $MY_VAR' // prints nothing or previous value ``
Important: withEnv sets the environment for the closure but does not modify env object. To set a variable permanently, use env.MY_VAR = 'value'.
Common Pattern: Use withEnv to override PATH or other system variables: ``groovy withEnv(['PATH+MY_TOOL=/opt/my-tool/bin']) { sh 'my-tool --version' } ``
Production Insight: We used withEnv to set AWS_DEFAULT_REGION for a specific step, but the next step also needed it. We had to repeat withEnv or use env.AWS_DEFAULT_REGION = 'us-east-1' outside. For a one-off, withEnv is perfect.
env.VAR, set it directly with env.VAR = 'value'.withEnv to set JAVA_HOME for a Maven build. The Maven process inherited the correct JAVA_HOME, but later steps that checked env.JAVA_HOME still saw the old value. We switched to env.JAVA_HOME = '/usr/lib/jvm/java-11' for consistency.withEnv for temporary overrides, env.VAR = for permanent changes.Dynamic Environment Variable Patterns
Sometimes you need to set environment variables dynamically based on conditions or external data. Here are production patterns:
1. Setting from file content: ``groovy pipeline { agent any stages { stage('Load Config') { steps { script { def config = readFile('config.json') def json = new groovy.json.``JsonSlurperClassic().parseText(config) env.DB_URL = json.db.url } } } } }
2. Conditional variable: ``groovy environment { DEPLOY_ENV = BRANCH_NAME == 'main' ? 'production' : 'staging' } ``
3. Using withEnv with dynamic keys: ``groovy def vars = [:] vars['MY_VAR'] = 'value' withEnv(vars.collect { k, v -> "$k=$v" }) { sh 'echo $MY_VAR' } ``
4. Reading from external source (e.g., Vault): ``groovy pipeline { agent any environment { // Using a plugin like HashiCorp Vault SECRET = vault path: 'secret/data/myapp', key: 'password' } stages { stage('Use Secret') { steps { sh 'echo $SECRET' } } } } ``
Production Insight: We dynamically set BUILD_TAG based on branch and timestamp to ensure unique artifact names. We used env.BUILD_TAG = "${BRANCH_NAME}-${BUILD_NUMBER}-${env.BUILD_ID}" but replaced BUILD_ID with currentBuild.startTimeInMillis.
currentBuild.startTimeInMillis gives a precise timestamp. Combine with BRANCH_NAME for unique tags.readYaml and set them with env. This allowed configuration without modifying the pipeline code.env. to set them globally.environment {} Block Directives
The environment {} block in Declarative Pipeline has several directives:
- Direct assignment:
MY_VAR = 'value' - From another variable:
MY_VAR = "${env.OTHER_VAR}" - From credential:
MY_CRED = credentials('credential-id') - From function:
MY_VAR = someFunction()
Credentials directive: When you use credentials('id'), Jenkins creates two environment variables: MY_CRED_USR and MY_CRED_PSW (for username-password credentials) or just MY_CRED (for secret text). For secret files, it sets the path.
Example: ``groovy pipeline { agent any environment { DOCKER_CREDS = credentials('docker-hub-creds') } stages { stage('Login') { steps { sh 'docker login -u $DOCKER_CREDS_USR -p $DOCKER_CREDS_PSW' } } } } ``
Important: The variable name you choose (e.g., DOCKER_CREDS) becomes the prefix. You cannot change the suffix _USR and _PSW.
Production Insight: We accidentally used credentials('id') with a secret text credential that didn't have username/password, so _USR and _PSW were not set. We switched to a username-password credential type.
_USR and _PSW. Secret text generates only the variable itself.environment { AWS_ACCESS_KEY = credentials('aws-creds') } but the credential was secret text; we got the key but no secret key. We changed to separate credentials for access key and secret key.Credentials-to-Environment Mapping
Jenkins provides a secure way to inject credentials as environment variables. The binding in credentials()environment {} block maps a credential ID to one or two environment variables.
- Username with password:
MY_VAR = credentials('id')createsMY_VAR_USR(username) andMY_VAR_PSW(password). - Secret text:
MY_VAR = credentials('id')createsMY_VARcontaining the secret. - Secret file:
MY_VAR = credentials('id')createsMY_VARcontaining the path to the file. - Certificate: Similar to username-password, but the password is the certificate password.
Example with multiple credentials: ``groovy pipeline { agent any environment { GIT_CREDS = credentials('github-ssh') DOCKER_CREDS = credentials('docker-hub') } stages { stage('Checkout') { steps { // GIT_CREDS_USR and GIT_CREDS_PSW available sh 'git clone https://$GIT_CREDS_USR:$GIT_CREDS_PSW@github.com/org/repo.git' } } } } ``
Production Insight: We had a credential that was a secret file containing a JSON. The MY_VAR pointed to the file path, but our script expected the content. We used readFile to read the file content and set another variable.
readFile in a script block.credentials('id') for a secret text but forgot to mask the variable in logs. We added sh 'set +x; echo $MY_VAR; set -x' to avoid exposure.credentials() for secure injection; never echo credentials in plain text.Common Pitfall: BUILD_ID Deprecation
BUILD_ID was deprecated in Jenkins 2.0 (released in 2016) and replaced by BUILD_NUMBER. However, BUILD_ID still exists for backward compatibility. The problem is that its value may not match the actual build number. In some plugins, BUILD_ID returns the build number as a string; in others, it returns a different identifier. This inconsistency can cause silent data corruption.
Production Incident: We used $BUILD_ID in a shell script to tag Docker images. After a Jenkins upgrade, BUILD_ID started returning '1' for all builds, overwriting the same tag. We lost the ability to rollback because all images had the same tag.
Fix: Replace all occurrences of $BUILD_ID with $BUILD_NUMBER. Also check plugins that might set BUILD_ID differently.
Detection: Run echo BUILD_ID=$BUILD_ID BUILD_NUMBER=$BUILD_NUMBER in a build and compare.
Best Practice: In new pipelines, never use BUILD_ID. If you see it in legacy code, refactor immediately.
BUILD_ID equals BUILD_NUMBER and fails if not. This caught the issue before deployment.Common Pitfall: WORKSPACE on Agents
The WORKSPACE environment variable points to the workspace directory on the node executing the build. On agents, this is the agent's workspace path, not the master's. However, there are nuances:
- Master vs Agent: If the build runs on master,
WORKSPACEis on master. If on an agent, it's on the agent. Never assume the path is the same. - Symbolic links: Some configurations use symbolic links for workspaces, which can cause
WORKSPACEto resolve differently. - Custom workspace: If you set a custom workspace via
ws('path'),WORKSPACEchanges to that path within thewsblock.
Production Incident: We had a pipeline that used WORKSPACE in a script that copied files to a network share. The script assumed the workspace was on the master, but the build ran on an agent. The copy failed because the path didn't exist on the master.
Fix: Use env.WORKSPACE in pipeline steps to get the correct path. In shell scripts, $WORKSPACE is correct but ensure the agent has the same filesystem.
Best Practice: Always use env.WORKSPACE in Groovy code. In shell, use $WORKSPACE but be aware of agent context.
env.WORKSPACE or $WORKSPACE to get the dynamic path.ws('custom-workspace') to override workspace. Later steps that used $WORKSPACE got the custom path, but we forgot to revert, causing confusion. We used ws blocks carefully.Common Pitfall: JENKINS_HOME Not Available on Agents
JENKINS_HOME is the home directory of the Jenkins master, typically /var/jenkins_home. This variable is set only on the master node. On agents, JENKINS_HOME is not set by default. If your pipeline tries to access $JENKINS_HOME on an agent, it will be empty or undefined.
Production Incident: A pipeline step that read a configuration file from $JENKINS_HOME/config.xml worked on master but failed on agents because the variable was empty. The file didn't exist on agents anyway.
Fix: Do not rely on JENKINS_HOME on agents. If you need to access master files, use the http:// API or copy files via plugins like Copy Artifact or External Workspace Manager.
Alternative: Use Jenkins.instance.getRootDir() in a script block on master, but this won't work on agents.
Best Practice: Avoid using JENKINS_HOME in pipeline steps that may run on agents. If you must, ensure the step runs on master via agent { label 'master' }.
$JENKINS_HOME. It worked in development (master) but failed in production (agents). We moved the config to a shared library resource or used the Config File Provider plugin.Common Pitfall: Environment Variable Leakage Between Stages
In Declarative Pipeline, variables set in a stage-level environment {} block are scoped to that stage. However, variables set via script { env.VAR = 'value' } are global and persist across stages. This can cause unintended side effects.
Symptom: A variable set in one stage is unexpectedly available in another stage, causing conflicts.
Example: ``groovy pipeline { agent any stages { stage('Set Var') { steps { script { env.MY_VAR = 'old_value' } } } stage('Override') { environment { MY_VAR = 'new_value' } steps { sh 'echo $MY_VAR' // prints 'new_value' } } stage('Check') { steps { sh 'echo $MY_VAR' // prints 'old_value' (global) } } } } ``
Fix: Use stage-level environment for temporary overrides, and avoid setting global variables unless necessary. If you need a global variable, use a unique name to avoid collisions.
Best Practice: Prefix global variables with the stage name or pipeline name, e.g., BUILD_MY_VAR.
env.DB_URL to a test database, and stage 'Deploy' also used env.DB_URL expecting production. The test value leaked into deploy. We renamed the test variable to TEST_DB_URL.Common Pitfall: Environment Variables Not Available in Parallel Stages
In Declarative Pipeline, parallel stages each run in their own context. Environment variables set in one parallel branch are not visible in another. Also, stage-level environment in a parallel block is isolated.
Symptom: A variable set in one parallel branch is empty in another branch.
Example: ``groovy pipeline { agent any stages { stage('Parallel') { parallel { stage('Branch A') { environment { MY_VAR = 'A' } steps { sh 'echo $MY_VAR' // prints 'A' } } stage('Branch B') { steps { sh 'echo $MY_VAR' // prints nothing or pipeline-level value } } } } } } ``
Fix: If you need to share variables between parallel branches, set them at the pipeline level or use a shared workspace file.
Best Practice: Avoid relying on cross-branch variable sharing. Use artifacts or stash/unstash to pass data.
env.TAG at pipeline level using currentBuild.startTimeInMillis to ensure uniqueness across branches.BUILD_ID Overwrites Docker Tag in Production
$BUILD_ID was the same as $BUILD_NUMBER.BUILD_ID is deprecated and returns the build number as a string but can be inconsistent; in this case, it returned '1' due to a plugin interaction.$BUILD_ID with $BUILD_NUMBER in the shell script and all pipeline references.- Never use
BUILD_IDin production; always useBUILD_NUMBERfor the numeric build ID.
echo $BUILD_ID shows '1' instead of '45'.$BUILD_ID with $BUILD_NUMBER. Run echo $BUILD_NUMBER to verify.WORKSPACE variable points to master's workspace on an agent build, causing file not found errors.env.WORKSPACE in pipeline steps. In shell, $WORKSPACE is correct. Ensure agent has workspace directory.echo $MY_VAR returns empty.environment {} block or use env.MY_VAR = 'value' in script step. Stage variables are scoped to that stage only.withEnv block in Scripted Pipeline does not persist variable after block ends.env.MY_VAR = 'value' to set a variable outside the block if needed.echo $BUILD_NUMBERecho $BUILD_IDPrint-friendly master reference covering all topics in this track.
Key takeaways
BUILD_ID; always use BUILD_NUMBER for build number.env.VAR in Groovy, $VAR in shell steps.withEnv for temporary overrides in Scripted Pipeline._USR and _PSW suffixes for username-password.WORKSPACE is dynamic and node-specific; always reference it dynamically.JENKINS_HOME is not available on agents.Common mistakes to avoid
6 patternsUsing `BUILD_ID` instead of `BUILD_NUMBER`
$BUILD_ID with $BUILD_NUMBERAssuming `WORKSPACE` is on master when build runs on agent
env.WORKSPACE dynamicallyUsing `env.VAR` in shell step without interpolation
env.VAR printedsh "echo ${env.VAR}"Setting stage-level environment and expecting it to persist
script { env.VAR = 'val' } for global scopeUsing `JENKINS_HOME` in agent builds
Not masking credentials in logs
sh 'set +x; ...; set -x' or credential masking pluginInterview Questions on This Topic
What is the difference between `BUILD_ID` and `BUILD_NUMBER` in Jenkins?
BUILD_ID is deprecated and may return an inconsistent value; BUILD_NUMBER is the reliable numeric build number. Always use BUILD_NUMBER.Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Everything here is grounded in real deployments.
That's Jenkins. Mark it forged?
8 min read · try the examples if you haven't