Skip to main content

Job conditions

The if field on a job controls whether that job runs. It holds a boolean expression that is evaluated once the job's dependencies have finished. If the expression is true the job runs; if it is false the job is skipped.

jobs:
train:
steps:
- run: ./train.sh

notify-failure:
depends_on: [train]
if: failure()
steps:
- run: ./notify.sh "training failed"

deploy:
depends_on: [train]
if: inputs.deploy == 'true'
steps:
- run: ./deploy.sh

The same if field, with the same expression language, is available on steps - see Step conditions.

Expression syntax

An if value is a boolean expression written directly in the field. It is not wrapped in ${{ }}: the ${{ }} form is string substitution (see Expressions) and is a different mechanism. A condition is evaluated to true or false, not substituted into text.

if: always() && deps.train.state == 'FAILED'

${{ }} is not interpreted inside if and is rejected during validation. Reference values directly (inputs.deploy), not as ${{ .inputs.deploy }}.

Operators

OperatorMeaning
==equal
!=not equal
&&logical and
||logical or
!logical not
( )grouping

Precedence, from highest to lowest: !, then == and !=, then &&, then ||. Use parentheses to make grouping explicit:

if: always() && (deps.build.state == 'FAILED' || deps.test.state == 'FAILED')

Literals

  • Strings, in single quotes: 'SUCCEEDED', 'prod'.
  • Booleans: true, false.
  • Integers: 3, 0.

Bracket form for references

A name that is a plain identifier - letters, digits, and underscores - can be written directly after a dot: deps.build.state, inputs.deploy. A name containing other characters, such as a hyphen, is written as a quoted name in brackets:

if: always() && deps['list-engines'].state == 'FAILED'

The two forms are equivalent; deps.build.state and deps['build'].state reference the same dependency. The bracket form works for any reference, so it also covers inputs['...'] and env['...'] names that are not plain identifiers.

Values you can reference

inputs.<name>

A workflow input. See Workflow Inputs.

if: inputs.environment == 'production'

env.<NAME>

An environment variable visible to the job. See Environment Variables.

if: env.STAGE == 'prod'

deps.<id>.state

The terminal state of a job this job depends on. <id> must be listed in this job's depends_on; referencing a job that is not a dependency is a validation error, because a non-dependency's state is not known when the condition is evaluated.

The possible state values are:

ValueMeaning
'SUCCEEDED'the dependency completed successfully
'FAILED'the dependency failed (a canceled dependency also reports 'FAILED')
'SKIPPED'the dependency was skipped

Comparing a dependency to 'SUCCEEDED' duplicates the success requirement, so deps.<id>.state is most useful together with always() or failure() - which lift that requirement - to branch on which dependency reached a given state:

report:
depends_on: [build, test]
if: always() && deps.test.state == 'FAILED'
steps:
- run: ./report.sh

deps.<id>.outputs.<name>

A value from a dependency's declared outputs - branching on an upstream job's computed value, not just its status. <id> must be listed in depends_on, and <name> must be an output that job declares (a typo or undeclared name is a validation error). An output that was not produced at runtime is an empty string.

jobs:
detect:
outputs:
changed: ${{ .steps.scan.outputs.changed }}
steps:
- id: scan
run: echo "changed=true" >> "$H2O_WORKFLOWS_OUTPUT"

deploy:
depends_on: [detect]
if: deps.detect.outputs.changed == 'true'
steps:
- run: ./deploy.sh

Referencing the outputs of a matrix dependency is not supported.

Status functions

Two functions summarize the outcome of a job's dependencies.

failure()

True when any job in depends_on ended in state FAILED, and it lifts the success requirement so the job can run despite that failure. For a job with depends_on: [a, b] it is equivalent to:

if: failure()
if: always() && (deps.a.state == 'FAILED' || deps.b.state == 'FAILED')

always()

Always true. The job runs regardless of its dependencies' outcomes.

cleanup:
depends_on: [train]
if: always()
steps:
- run: ./cleanup.sh

The success requirement

By default, a job runs only after all of its depends_on jobs succeed. Adding an if condition keeps that success requirement and applies it together with the condition: the job runs when all dependencies succeeded and the condition is true.

The failure() and always() functions are the exception: using either one lifts the success requirement, so the job can run even when a dependency failed or was skipped. This is what makes failure-handling and cleanup jobs possible. Any other condition is applied on top of the requirement, so a condition that happens to be always true still runs only when the dependencies succeeded:

ConditionRuns when
(no if)all dependencies succeeded
if: inputs.deploy == 'true'all dependencies succeeded and the condition is true
if: trueall dependencies succeeded (an always-true condition does not lift the requirement)
if: failure()any dependency failed
if: always()any outcome (the success requirement is lifted)

Matrix jobs

When a job defines a matrix, its if is evaluated per instance - each generated instance decides independently whether it runs.

When a job depends on a matrix job, deps.<id>.state is the aggregate across all of that matrix's instances: 'SUCCEEDED' only if every instance succeeded, and 'FAILED' if any instance failed. This matches the rule that depending on a matrix job waits for all of its instances.

Skipped jobs

A job whose condition excludes it ends in the terminal state SKIPPED. A skipped job:

  • does not fail the run - a run whose jobs are all succeeded or skipped is successful;
  • does not trigger cancel_on_failure;
  • causes its own dependents to be skipped in turn, unless those dependents use failure() or always().

See Failure handling for how skipping interacts with cancel_on_failure.

Validation

Conditions are checked when the workflow is saved:

  • the expression must be syntactically valid;
  • every deps.<id> must name a job in this job's depends_on (a step-scope steps.<id> reference is not allowed in a job condition);
  • referenced inputs and env names must exist.

Examples

Route on success and failure:

jobs:
train:
steps:
- run: ./train.sh

notify-success:
depends_on: [train]
# no if: needed - a job runs by default only when its dependencies succeed
steps:
- run: ./notify.sh ok

notify-failure:
depends_on: [train]
if: failure()
steps:
- run: ./notify.sh fail

Gate a deploy on an input, still requiring the build to succeed:

deploy:
depends_on: [build]
if: inputs.deploy == 'true'
steps:
- run: ./deploy.sh

Always clean up, whatever happened upstream:

cleanup:
depends_on: [train]
if: always()
steps:
- run: ./cleanup.sh

Combining job and step conditions

A job condition and step conditions compose: the job-level if decides whether the job runs at all, and step-level if decides the flow within it. See Combining job and step conditions for a worked example.


Feedback