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 production

Write 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

==

VAR == value

Equality check

!=

VAR != value

Inequality check

>

COUNT > 0

Greater than

<

COUNT < 5

Less than

>=

SCORE >= 80

Greater or equal

<=

FAILURES <= 0

Less or equal

&&

A == true && B == true

Logical AND

||

A == true || B == true

Logical OR

glob()

glob(BRANCH_NAME, "release/*")

Pattern match

!

!(A == true)

Logical NOT

changesetInclude()

changesetInclude("src/**", "package.json")

True if any changed file matches any of the patterns

changesetExclude()

changesetExclude("docs/**")

True if at least one changed file doesn't match any of the patterns

Parentheses for Grouping

condition: state: (A == true || B == true) && ENABLED == false

Comparing 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 test

includePaths 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 directory

Patterns 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 api

You 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 test

The 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

output-variables

Not set. Scripts see them as empty, but a state condition that uses them fails the step

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 build

Feature Flag Gates

- step: name: Run E2E Tests condition: state: RUN_E2E == true script: - npm run test:e2e

Set 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 audit

Define 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 test

The 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_NAME in a previous step

  • Check for trailing whitespace in variable values

  • Comparisons are typed. Comparing to true or false requires the value to be exactly true or false; a value such as True causes 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~1 locally

  • On 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 state expression isn't defined when the step starts, or a string value is missing its double quotes and is being read as a variable name


 

Still need help?

The Atlassian Community is here for you.