Hardcoding API_URL, DB_PASSWORD, or DEPLOY_ENV in a Jenkinsfile or in the job config means the same code can't move from staging to production without editing, and secrets end up in git history and console logs. Injecting environment variables in Jenkins — via the Jenkins UI, the Declarative environment {} block, Scripted withEnv(), the EnvInject plugin, credentials() binding for secrets, or from a properties file or shell output — lets one pipeline run anywhere with the right values per environment, while keeping secrets masked and values scoped to the narrowest stage or block that needs them. Getting the scope wrong (global when job would do, or withEnv outside its block) is the top cause of "variable not found" between stages.
This complete guide covers all seven ways to inject env vars in Jenkins — setting via Jenkins UI (Manage Jenkins → System), environment {} in a Declarative Pipeline, withEnv() in a Scripted Pipeline, EnvInject plugin for older freestyle setups, injecting secrets and credentials as env vars with credentials() binding, injecting from a properties file or shell script output, and the global vs job-level vs stage-level scope differences — plus common errors (variable not found, scope between stages, single vs double quotes) and how to debug them — with references to Jenkins Pipeline Syntax, withEnv Step, Using Environment Variables, EnvInject Plugin, and Using Credentials.
environment { VAR = "value" } at the top level (job scope) or inside a stage (stage scope). For Scripted, use withEnv(['VAR=value']) { sh 'echo $VAR' } (block scope) or env.VAR = "value" for Groovy global. For secrets, use withCredentials([string(credentialsId: 'id', variable: 'TOKEN')]) so Jenkins masks the value. For bulk vars, use readProperties from a file or env.VAR = sh(returnStdout: true, script: 'echo hi').trim() from a shell. Prefer the narrowest scope — stage over job over global — and check the environment variable tools for generating and validating .env files that feed Jenkins.
What Are Environment Variables in Jenkins?
Environment variables in Jenkins are key-value pairs exposed as env.VAR in Groovy and as $VAR or ${VAR} in shell steps (sh, bat). They include built-ins (BUILD_NUMBER, BUILD_URL, JOB_NAME, WORKSPACE, GIT_COMMIT) and custom values for API URLs, feature flags, deployment targets, and secrets. Per Jenkins: Using Environment Variables, they can be set at global (Manage Jenkins → System), job/pipeline (environment {}), or block/stage (withEnv, stage environment, or withCredentials) scope, and are interpolated in Groovy strings and shell steps — but with critical quoting and scope rules that cause the most common errors.
Global vs Job-Level vs Stage-Level Scope — The Mental Model
Scope determines where the variable is visible and who can override it. Precedence is stage > job > global — the narrowest wins.
| Scope | Where Set | Visible To | Use For |
|---|---|---|---|
| Global | Manage Jenkins → System → Global properties → Environment variables | Every job, every run | Truly global constants (e.g., TOOLWASP_URL), rarely — avoid secrets here |
| Job / Pipeline | pipeline { environment { VAR="value" } } at top level | All stages of that pipeline | Pipeline-scoped config (e.g., API_URL) — preferred for most vars |
| Stage / Block | stage('Deploy') { environment { VAR="value" } } or withEnv(['VAR=value']) { ... } | That stage or that withEnv block only | Temporary or stage-specific (e.g., DEPLOY_ENV=canary for one stage) |
Precedence example: Global DEPLOY_ENV=staging, job DEPLOY_ENV=production → the pipeline sees production (job wins). Inside withEnv(['DEPLOY_ENV=canary']) { sh 'echo $DEPLOY_ENV' } → canary (stage/block wins). Outside the withEnv block, it reverts to production. This is why a variable set with withEnv is "not found" in the next stage — it was block-scoped and the block ended.
Rule: Define as narrowly as possible — stage if only one stage needs it, job if all stages need it, global only for truly global non-secrets. Global env vars are visible to every job and are not masked in logs, so never put secrets there; use credentials() for secrets.
7 Ways to Inject Environment Variables — When to Use Which
| Method | Syntax | Scope | Secrets Masked? | Best For |
|---|---|---|---|---|
| Jenkins UI (System) | Manage Jenkins → System → Environment variables | Global | No | Global constants (rare) |
Declarative environment {} | environment { API_URL="https://..." } | Job or Stage | Via credentials() | ★ Pipeline vars (most common) |
Scripted withEnv() | withEnv(['X=1']) { sh 'echo $X' } | Block | Via withCredentials | Scripted, temporary vars |
| EnvInject Plugin | Properties file / Groovy / UI | Job | No (legacy) | Old freestyle jobs |
credentials() binding | withCredentials([string(credentialsId: 'id', variable: 'TOKEN')]) | Block | Yes (masked as ****) | ★ Secrets |
| Properties file | readProperties file: 'env.properties' | Job | No (file content visible) | Bulk vars from file |
| Shell output | env.VAR = sh(returnStdout: true, script: 'echo hi').trim() | Job (Groovy) | No | Dynamic from script |
For secrets, always use credentials() or withCredentials — it masks the value as **** in console output. Plain environment {} or withEnv with a hardcoded secret is visible in the job config and, if echoed, in logs.
1. Setting Env Vars via Jenkins UI (Manage Jenkins → System)
Navigate to Manage Jenkins → System → Global properties → Environment variables → Add, enter Name: DEPLOY_ENV and Value: production, and save. The variable is then available as env.DEPLOY_ENV in Groovy and $DEPLOY_ENV in sh steps in every job. Per Jenkins System Configuration, this is global and requires Overall/Administer permission to change — a global change affects every job, so it is the broadest and riskiest scope.
When to use: Only for truly global, non-secret constants that every job needs — e.g., ARTIFACTORY_URL or TOOLWASP_URL. For job-specific values, prefer the pipeline's own environment {} so the value is versioned with the Jenkinsfile and not dependent on the controller's global state. Never put secrets here — they are not masked and are visible to anyone with job read permission via env in the build.
2. Declarative Pipeline — environment {} Block (Most Common ★)
The Declarative environment directive, documented at Pipeline Syntax: environment, defines env vars at the top level (job scope) or inside a stage (stage scope). Values can be literal, interpolated from other env vars, or from credentials() for secrets.
pipeline {
agent any
environment {
API_URL = "https://api.example.com"
NODE_ENV = "production"
// From credentials (secret, masked)
AWS_SECRET = credentials('aws-secret-key')
// Interpolation from another env var
FULL_URL = "https://${ENV}.example.com" // ENV from global or earlier
}
stages {
stage('Build') {
steps { sh 'echo $API_URL' } // sees https://api.example.com
}
stage('Deploy Canary') {
environment { DEPLOY_ENV = "canary" } // overrides job-level for this stage
steps { sh 'echo $DEPLOY_ENV' } // canary
}
stage('Deploy Prod') {
steps { sh 'echo $DEPLOY_ENV' } // production (job-level, since stage had no override)
}
}
}
Key details:
- Scope: Top-level
environment→ all stages; stage-levelenvironment→ that stage only and overrides top-level for that stage (precedence: stage > job > global). - Interpolation:
"https://${ENV}.example.com"uses Groovy double quotes with${ENV}— single quotes'https://${ENV}.example.com'do not interpolate and yield the literal string. This is the top source of "variable not found" when the value looks like${ENV}literally. - Credentials in environment:
AWS_SECRET = credentials('aws-secret-key')is the Declarative shorthand forwithCredentials— the value is masked as****in logs. For username/password, usecredentials('docker-hub')which exposesDOCKER_HUB_USRandDOCKER_HUB_PSW. - When not to use: For truly dynamic values computed at runtime (e.g., from a shell), use
env.VAR = sh(...).trim()in ascriptblock, sinceenvironment {}is evaluated at pipeline start and cannot callsh.
3. Scripted Pipeline — withEnv() and Groovy env
In Scripted Pipeline, per withEnv Step, withEnv sets env vars for a block:
node {
withEnv(['MY_VAR=hello', "OTHER=${env.BUILD_NUMBER}"]) {
sh 'echo $MY_VAR' // hello
sh 'echo $OTHER' // 123
}
sh 'echo $MY_VAR' // not found — outside withEnv block
}
Block scope is the critical difference from Declarative: withEnv vars disappear after the closing } — they are not visible in the next stage or even the next statement outside the block. This is why "variable not found between stages" happens with withEnv — the fix is to use environment {} (job scope) or Groovy global env.VAR = "value" which persists for the rest of the run:
node {
env.MY_VAR = "hello" // Groovy global, visible for the rest of the run
stage('One') { sh 'echo $MY_VAR' } // hello
stage('Two') { sh 'echo $MY_VAR' } // still hello
}
Groovy env is the most flexible for Scripted — it can be set from any Groovy expression, including shell output: env.GIT_HASH = sh(script: 'git rev-parse HEAD', returnStdout: true).trim(). Unlike withEnv, it is not automatically cleaned up, so it persists until the run ends or is overwritten.
4. EnvInject Plugin — Still Widely Used in Older Freestyle Setups
The EnvInject Plugin predates Pipeline and is still common in freestyle jobs and older controllers. It injects env vars at job startup from a properties file, a Groovy script, or key-value pairs in the job config. For Pipeline, it is largely superseded by environment {} and withEnv, but it remains in freestyle where Pipeline directives are not available.
How it works for freestyle: In the job config → Build Environment → Inject environment variables, set Properties File Path (env.properties) or Properties Content (MY_VAR=hello), or a Groovy script that returns a map. At build start, EnvInject loads the file or script and injects the vars for that build.
When to use vs avoid: Use for freestyle jobs that cannot be converted to Pipeline yet. For new Pipeline jobs, prefer environment {} (Declarative) or withEnv/env (Scripted) — they are versioned with the Jenkinsfile, work with credentials(), and don't require a plugin that is in maintenance mode. EnvInject does not mask secrets in logs, so don't use it for secrets — use credentials().
5. Injecting Secrets and Credentials as Env Vars — credentials() Binding (The Secure Way)
Never put secrets in plain environment {} or withEnv — they are visible in the job config (for Declarative, in the Jenkinsfile if hardcoded) and, if echoed, in console logs. Jenkins Credentials with binding is the secure path: the secret is stored encrypted in Jenkins (via the Credentials plugin), referenced by ID, and masked as **** in logs.
Declarative — credentials() in environment {}
pipeline {
agent any
environment {
// Secret text (e.g., API token)
AWS_SECRET = credentials('aws-secret-key')
// Username/password — exposes two vars: DOCKER_HUB_USR and DOCKER_HUB_PSW
DOCKER_HUB = credentials('docker-hub-creds')
}
stages {
stage('Deploy') {
steps {
sh 'echo $AWS_SECRET' // logs as ****, not the real value
sh 'echo $DOCKER_HUB_USR' // username, not masked separately? Actually both masked
}
}
}
}
Scripted — withCredentials
withCredentials([
string(credentialsId: 'aws-secret-key', variable: 'AWS_SECRET'),
usernamePassword(credentialsId: 'docker-hub', usernameVariable: 'USER', passwordVariable: 'PASS')
]) {
sh 'echo $AWS_SECRET' // ****
sh 'docker login -u $USER -p $PASS'
}
Supported bindings include string (secret text), usernamePassword, file (secret file), and sshUserPrivateKey. The variables are only available inside the environment or withCredentials block and are masked — even sh 'env | sort' will show **** for them. For non-secret vars, use plain environment or withEnv.
6. Injecting Vars from a Properties File or Shell Script Output
From a Properties File — readProperties
For bulk vars, a .properties or .env file is cleaner than dozens of environment {} lines. In Declarative, use a script block with readProperties:
pipeline {
agent any
stages {
stage('Load Env') {
steps {
script {
def props = readProperties file: 'env.properties' // key=value per line
env.API_URL = props['API_URL']
env.NODE_ENV = props['NODE_ENV']
}
sh 'echo $API_URL' // available for the rest of the run (env is global in Groovy)
}
}
}
}
The file is typically API_URL=https://api.example.com\nNODE_ENV=production — the same format as .env and loadable via the environment variable tools that generate and validate .env files from CSV or JSON. Note: readProperties is a Pipeline step that reads from the workspace — the file must be checked out first (checkout scm is implicit in Declarative).
From Shell Script Output — sh(returnStdout: true)
For dynamic values computed at runtime (e.g., Git hash, build timestamp, or a value from a script):
pipeline {
agent any
stages {
stage('Set Dynamic') {
steps {
script {
env.GIT_HASH = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
env.BUILD_TIME = sh(script: 'date -u +%Y-%m-%dT%H:%M:%SZ', returnStdout: true).trim()
}
sh 'echo $GIT_HASH' // e.g., a1b2c3d
}
}
}
}
The .trim() is essential — sh with returnStdout: true includes the trailing newline, which would otherwise become part of the variable (e.g., "a1b2c3d\n") and break URLs or tags. This pattern is also how to bridge EnvInject-style file injection in Pipeline: write the shell output to a file, then readProperties it.
Common Errors — Variable Not Found and Scope Between Stages
1. withEnv Variable Not Found in the Next Stage
Symptom: withEnv(['MY_VAR=hello']) { sh 'echo $MY_VAR' } works, but the next stage's sh 'echo $MY_VAR' prints empty.
Cause: withEnv is block-scoped — the variable disappears after the closing }.
Fix: For job-wide vars, use top-level environment { MY_VAR="hello" } (Declarative) or env.MY_VAR = "hello" (Scripted Groovy global) which persists for the rest of the run.
2. Single Quotes Don't Interpolate — Variable Literally ${ENV}
Symptom: environment { URL = 'https://${ENV}.example.com' } yields the literal string https://${ENV}.example.com, not https://prod.example.com.
Cause: Groovy single quotes ('...') are plain strings; double quotes ("...") are GStrings that interpolate ${VAR}. The same applies to sh 'echo $VAR' vs sh "echo $VAR" — but note: sh "echo $VAR" interpolates in Groovy before the shell sees it, so sh "echo ${env.VAR}" is often safer to let the shell expand.
Fix: Use double quotes for interpolation: environment { URL = "https://${ENV}.example.com" } and sh "echo ${env.URL}" or sh 'echo $URL' (shell expansion, not Groovy).
3. Env Var Set in One Stage Not Visible in the Next
Symptom: stage('One') { steps { script { env.FOO = "bar" } } } then stage('Two') { steps { sh 'echo $FOO' } } sometimes shows empty in Declarative.
Cause: In Declarative, each stage can run on a different agent — env is Groovy global per run, but if stages use different agent labels, the env may not propagate as expected. Also, env set inside a script block is Groovy global, but its timing relative to Declarative's environment evaluation matters.
Fix: Set job-level environment { FOO="bar" } at the top, or use withEnv around the stages that need it, or stash the value in a file and readProperties in the next stage.
4. Properties File Not Loaded — "No such file" or Empty
Symptom: readProperties file: 'env.properties' fails with "No such file" or returns empty.
Cause: The file hasn't been checked out yet, or the path is wrong, or the file uses the wrong delimiter (key: value instead of key=value).
Fix: Ensure checkout scm (implicit in Declarative) has run before readProperties, use key=value per line without spaces around :, and handle line endings (CRLF vs LF) — the plugin handles both, but hand-written files with : will be parsed as one key.
5. Credentials Not Masked — Secret Visible in Log
Symptom: sh 'echo $MY_SECRET' where MY_SECRET was set via plain environment { MY_SECRET="s3cr3t" } prints the real value.
Cause: Only credentials() / withCredentials vars are masked; plain env vars are not, even if the value is a secret.
Fix: Store the secret in Jenkins → Manage Jenkins → Credentials, then reference via credentials('id') in environment or withCredentials — the log will show ****.
6. EnvInject Works in Freestyle but Not in Pipeline
Symptom: EnvInject injects in a freestyle job but the same properties file does nothing in a Pipeline.
Cause: EnvInject is a freestyle Build Environment; Pipeline has its own environment and withEnv that do not read EnvInject's injection.
Fix: For Pipeline, migrate to environment {} or readProperties + env; keep EnvInject only for freestyle jobs that cannot be converted.
Best Practices — Where to Define What
| Var Type | Where to Define | Why |
|---|---|---|
| Non-secret, pipeline-scoped | environment {} at top level | Versioned with Jenkinsfile, visible to all stages |
| Non-secret, stage-scoped | environment {} inside that stage or withEnv around the stage | Least blast radius, no leak to other stages |
| Secret (any scope) | credentials() in environment or withCredentials block | Encrypted at rest, masked in logs, auditable |
| Bulk from file | readProperties + env.KEY = value in a script block | One file for many vars, still versioned |
| Dynamic from shell | env.VAR = sh(returnStdout: true, script: '...').trim() | Computed at runtime (e.g., Git hash) |
Per Jenkins Pipeline Best Practices and CloudBees: Best Practices, keep the Jenkinsfile declarative where possible (for Blue Ocean visualization and restartability), use Scripted only when withEnv or Groovy logic is needed, and never put secrets in plain env vars or in the global System config.
FAQs About Injecting Env Vars in Jenkins
How do I set an environment variable for all Jenkins jobs?
Use Manage Jenkins → System → Global properties → Environment variables for a global var, but prefer job-level environment {} for pipeline-specific values to avoid global leakage. Global vars require admin and affect every job.
What is the difference between environment { } and withEnv()?
environment {} is Declarative, job or stage scoped, and evaluated at pipeline start (no sh inside). withEnv() is Scripted, block scoped, and can wrap any block including sh. For job-wide vars, environment {} persists; withEnv disappears after its closing brace.
How do I use secrets as env vars in Jenkins?
Store the secret in Manage Jenkins → Credentials, then reference via environment { TOKEN = credentials('token-id') } (Declarative) or withCredentials([string(credentialsId: 'token-id', variable: 'TOKEN')]) { ... } (Scripted). Jenkins masks the value as **** in logs.
How do I load env vars from a properties file?
In Pipeline, use script { def props = readProperties file: 'env.properties'; env.KEY = props['KEY'] } after checkout. The file is KEY=VALUE per line. For freestyle, EnvInject can load the file at build start.
Why is my env var not found in the next stage?
It was likely set with withEnv (block scope) or inside one stage's script without persisting. Use top-level environment {} or Groovy global env.VAR set in an early stage (or before stages) so it persists for the run.
Should I use EnvInject for Pipeline?
No — EnvInject is for freestyle; for Pipeline, use environment {}, withEnv, or readProperties + env. EnvInject is in maintenance mode for Pipeline and does not mask secrets.
Conclusion
Injecting environment variables in Jenkins is choosing the narrowest scope that fits — global via System for truly global constants, job-level environment {} for pipeline-wide config, stage-level environment or withEnv for temporary values, and credentials() / withCredentials for anything secret — with properties files and shell output bridging bulk and dynamic cases. Understanding that withEnv is block-scoped, that single quotes don't interpolate, and that global is visible everywhere prevents the "variable not found between stages" that is the most common Jenkins env var bug.
Start with environment {} for the pipeline's non-secret config, add credentials() for secrets, and reach for withEnv or readProperties only when the value is temporary or from a file — then verify the scope with an env | sort in a sh step before the next stage needs it.