self-host →

Conditions for GitHub Actions jobs and steps

Conditional logic in GitHub Actions: control job and step execution with if statements, expressions, and context variables to build smarter workflows.

GitHub Actions workflows don’t have to be simple linear sequences of jobs or steps. Using conditions, you can make your workflows smarter by controlling exactly when jobs and steps should run. This article will show you how to add logic and decision-making to your workflows.

Job-level conditions#

Only want to deploy when code hits the main branch? Use GitHub’s ref context:

jobs:
deploy:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "Deploying to production"

Smart job dependencies#

Sometimes you want jobs to run even when their dependencies are skipped. Here’s how to handle a deployment that should run when tests are skipped (but not when they fail):

jobs:
test:
if: github.event_name != 'workflow_dispatch' # Skip tests on manual trigger
runs-on: ubuntu-latest
steps:
- run: echo "Running tests"
deploy:
needs: test
if: ${{ !failure() && !cancelled() }}
runs-on: ubuntu-latest
steps:
- run: echo "Deploying since tests passed or were skipped"

Step-level conditions#

Control step execution based on previous outputs - useful for multi-stage deployments:

steps:
- id: check_env
run: |
echo "status=ready" >> $GITHUB_OUTPUT
- if: ${{ steps.check_env.outputs.status == 'ready' }}
run: echo "Environment is ready for deployment"

Key contexts for conditions#

  • github - Access branch names, event types, commit info
  • needs - Check status of dependent jobs
  • steps - Read previous step outputs and status
  • env - Use environment variables
  • job - Get info about current job
  • runner - Access runner environment details

Status check functions#

  • success() - Green light - previous steps passed
  • failure() - Red light - something failed upstream
  • cancelled() - Someone hit the cancel button
  • always() - Run regardless of previous status (use carefully!)

The default success() check disappears when you add your own if. By default, every step and job carries an implicit success() condition, so it only runs when everything before it passed. The moment you write a custom if, that implicit check is dropped. If you still want the step to run only on success, include success() yourself — and if you want it to run on failure, you must spell out failure() (otherwise the step is silently skipped when something upstream fails).

Expression syntax#

Expressions use ${{ <expression> }} syntax (optional in if conditions). Chain conditions with && (AND) and || (OR):

steps:
- if: |
github.ref == 'refs/heads/main' &&
github.event_name == 'push' &&
success()
run: echo "Running production deploy on main branch push"

Resources#