Dynamic step conditions
Dynamic Step Conditions allow steps to be skipped automatically based on variable values at runtime. When a condition evaluates to false, the step is marked as skipped (not failed), and the pipeline continues. This is the primary mechanism for skipping unnecessary work — particularly in monorepos where only changed services need to be built and tested. If the condition can't be evaluated, for example because a variable it uses doesn't exist, the step fails instead.
Syntax
- step:
name: My Step
condition:
changesets:
includePaths:
- "path/to/watch/**"Or using variable-based state expressions:
- step:
name: My Step
condition:
state: VARIABLE_NAME == "expected_value"Condition Types
1. Variable State Conditions
Evaluate a boolean expression against pipeline variables, such as output variables from earlier steps, repository and workspace variables, and built-in variables like BITBUCKET_BRANCH using condition: state::
- step:
name: Deploy to Production
condition:
state: DEPLOY_ENABLED == true
script:
- ./deploy.sh productionWrite variable names without the $ prefix. Write true, false, and numbers without quotes, and put string values in double quotes, for example BITBUCKET_BRANCH == "main". An unquoted word is read as a variable name. Every variable used in the expression must exist when the step starts, otherwise the step fails instead of being skipped. Secured variables can't be used.
Supported Operators
Operator | Example | Description |
|---|---|---|
|
| Equality check |
|
| Inequality check |
|
| Greater than |
|
| Less than |
|
| Greater or equal |
|
| Less or equal |
|
| Logical AND |
|
| Logical OR |
|
| Pattern match |
|
| Logical NOT |
|
| True if any changed file matches any of the patterns |
|
| True if at least one changed file doesn't match any of the patterns |
Parentheses for Grouping
condition:
state: (A == true || B == true) && ENABLED == falseComparing to Empty Values
condition:
state: SERVICE_CHANGED != "" # Run if SERVICE_CHANGED is set to a non-empty value
condition:
state: SKIP_TESTS != true # Run unless SKIP_TESTS is true (SKIP_TESTS must be defined)
Both variables must exist when the step starts. A variable that isn't defined causes an evaluation error and fails the step, so define opt-out toggles such as SKIP_TESTS as repository variables with a default value of false.
2. Changeset Conditions
Run a step only when specific files have changed in the triggering commit:
- step:
name: Test Payment API
condition:
changesets:
includePaths:
- "services/payment-api/**"
script:
- cd services/payment-api && npm testincludePaths Glob Patterns
condition:
changesets:
includePaths:
- "src/**" # Any file under src/
- "package.json" # Specific file
- "**/*.ts" # TypeScript files in any subdirectory (not the repository root)
- "*.ts" # TypeScript files in the repository root
- "services/api/**" # Subtree of a directoryPatterns use Unix glob matching: * matches any characters within one path segment, ** matches across path segments, and ? matches a single character. For example, **/*.ts matches TypeScript files in subdirectories but not in the repository root.
Multiple Paths (OR logic)
Including multiple paths means the step runs if any of the paths have changed:
condition:
changesets:
includePaths:
- "src/**"
- "package.json"
- "package-lock.json"You can use excludePaths instead of includePaths to run a step when at least one changed file is outside the listed paths. A condition can use includePaths or excludePaths, but not both.
Combining Variable and Changeset Conditions
Variable expressions and changeset conditions can be combined in a single condition block. When both are present, the step only runs if both conditions are met:
# Step only runs if API files changed AND deploy is enabled
- step:
name: Deploy API
condition:
changesets:
includePaths:
- "services/api/**"
state: DEPLOY_ENABLED == true
script:
- ./deploy.sh apiYou can also use variable-based conditions alone when you need complex logic:
# Pattern: Use change detection step to set variables, then use variable conditions
- step:
name: Detect Changes
script:
- |
if git diff --name-only HEAD~1 | grep -q "^services/api/"; then
echo "API_CHANGED=true" >> $BITBUCKET_PIPELINES_VARIABLES_PATH
else
echo "API_CHANGED=false" >> $BITBUCKET_PIPELINES_VARIABLES_PATH
fi
output-variables:
- API_CHANGED
- step:
name: Test API
condition:
state: API_CHANGED == true
script:
- cd services/api && npm testThe detection step always writes API_CHANGED as true or false. If the variable weren't written, the state condition would fail the Test API step instead of skipping it.
Step Behavior When Condition Is False
Behavior | Description |
|---|---|
Step status | Skipped (shown in UI, not failed) |
Pipeline continues | ✅ Yes — subsequent steps still run |
Artifacts | Not produced (step didn't run) |
Build minutes | Not consumed |
| Not set. Scripts see them as empty, but a |
If a later step uses variables from a skipped step, those variables don't exist. In scripts they're empty, but a state condition that references them fails the step. Make sure variables used in conditions are always written, or are defined as repository variables with a default value.
Common Patterns
Monorepo Service Isolation
pipelines:
default:
- step:
name: Detect Changed Services
script:
- |
if git diff --name-only HEAD~1 | grep -q "^services/payment/"; then
echo "PAYMENT_CHANGED=true" >> $BITBUCKET_PIPELINES_VARIABLES_PATH
else
echo "PAYMENT_CHANGED=false" >> $BITBUCKET_PIPELINES_VARIABLES_PATH
fi
if git diff --name-only HEAD~1 | grep -q "^services/email/"; then
echo "EMAIL_CHANGED=true" >> $BITBUCKET_PIPELINES_VARIABLES_PATH
else
echo "EMAIL_CHANGED=false" >> $BITBUCKET_PIPELINES_VARIABLES_PATH
fi
output-variables:
- PAYMENT_CHANGED
- EMAIL_CHANGED
- parallel:
- step:
name: Build Payment Service
condition:
state: PAYMENT_CHANGED == true
script:
- cd services/payment && npm run build
- step:
name: Build Email Service
condition:
state: EMAIL_CHANGED == true
script:
- cd services/email && npm run buildFeature Flag Gates
- step:
name: Run E2E Tests
condition:
state: RUN_E2E == true
script:
- npm run test:e2eSet RUN_E2E=true as a repository variable when you want E2E tests enabled. Define RUN_E2E with a default value of false so the variable always exists. If it isn't defined, the step fails instead of being skipped.
Branch-Based Conditions
- step:
name: Deploy to Production
condition:
state: BITBUCKET_BRANCH == "main"
deployment: production
script:
- ./deploy.sh production"main" is in double quotes because it's a string value. Without quotes, main is read as a variable name and the step fails.
Security Scan Toggle
- step:
name: Security Scan
condition:
state: SKIP_SECURITY_SCAN != true # Run unless SKIP_SECURITY_SCAN is true
script:
- npm auditDefine SKIP_SECURITY_SCAN as a repository variable with a default value of false. If it isn't defined, the step fails instead of running.
Changeset Conditions on Pull Requests
For pull request pipelines, changeset conditions compare against the target branch (not HEAD~1):
pipelines:
pull-requests:
'**':
- step:
name: Test Changed Services
condition:
changesets:
includePaths:
- "src/**"
script:
- npm testThe step runs only if files under src/ were changed in the PR.
Constraints
Constraint | Value |
|---|---|
Condition expression max length | 1,000 characters |
Glob pattern depth | Unlimited |
Changeset detection on first commit | Compares against empty tree (all files match) |
Troubleshooting
Step always runs despite condition:
Confirm variable is set and has the expected value using
echo $VAR_NAMEin a previous stepCheck for trailing whitespace in variable values
Comparisons are typed. Comparing to
trueorfalserequires the value to be exactlytrueorfalse; a value such asTruecauses an evaluation error and fails the step
Step always skips despite changed files:
Verify the glob pattern matches your file paths exactly
Test the pattern with
git diff --name-only HEAD~1locallyOn the first commit to a repo, all files are considered changed
Parallel steps all skip unexpectedly:
If a previous step that sets variables was skipped, its output variables don't exist, so conditions that use them fail the step
Ensure the variable-setting step itself has no condition, or its condition will always be met
Step fails with "Unable to resolve value for variables":
A variable used in the
stateexpression isn't defined when the step starts, or a string value is missing its double quotes and is being read as a variable name
Related
Variable sharing reference — Pass variables between steps for condition logic
Tutorial 8: Conditional Execution — Hands-on guide
Dynamic pipelines reference — Generate pipelines programmatically when conditions are insufficient
Was this helpful?