In this episode we discuss how to arrange job execution order with needs, run jobs in parallel, and execute jobs and steps conditionally using if and GitHub Actions' built-in status functions.

In the previous episode 5 we discussed variables, contexts, and environment variables — how data flows within a job. But real-world pipelines almost never contain just one job. Production teams usually split work into several stages: lint, build, test, and deploy. The question that immediately arises: do all those stages run simultaneously? Should deploy wait for test to finish? What if you only want to send a notification when everything fails?
The answers to all those questions are in this episode: job dependencies with needs and conditional execution with if. These two mechanisms are the "traffic control" of a workflow — without them, your jobs run unordered and uncontrolled, like vehicles at an intersection without a traffic light.
GitHub Actions' default is parallel. When you define two jobs with no relationship, GitHub immediately runs both on different runners at the same time.
name: CI
on: [push]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4Without needs, the lint and test jobs don't wait for each other. This is great for efficiency: total pipeline time equals the duration of the longest job, not the sum of all jobs. It's like several chefs in a kitchen — each cooks a different dish simultaneously, not taking turns.
Tip
Because each job runs on a separate runner, one job cannot see another job's filesystem or variables. If you need to move data between jobs, that's not parallelism's business — we'll cover the solution (artifacts) in episode 8.
To make jobs run sequentially, use needs. This key declares which jobs must succeed first before this job starts.
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm ci
- run: npm run build
deploy:
runs-on: ubuntu-latest
needs: build
steps:
- run: ./deploy.shHere deploy only starts after build finishes. Two important things:
needs runs first.needs context, the dependent job can read the previous job's outputs, for example needs.build.outputs.version — a common pattern for passing a build version on to the deploy stage.Not every job or step must always run. The if key lets you execute a job or step only when certain conditions are met. Conditions are written as expressions evaluated to a boolean value.
The default condition of a job or step is success — it only runs if all previous steps succeeded. With if, you can deviate from this default using built-in status functions:
| Function | Runs when |
|---|---|
success() | All previous steps succeeded (default) |
failure() | A previous step failed |
always() | Always, whatever the result of previous steps |
cancelled() | The workflow was cancelled |
The failure() status function is very useful for notification jobs. You don't want to bombard the team with "pipeline succeeded" messages — messages are only needed when something is broken:
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm ci
- run: npm run build
notify:
runs-on: ubuntu-latest
needs: build
if: failure()
steps:
- name: Kirim alert ke chat
run: curl -s -X POST https://api.example.com/alertThe notify job only runs when build fails. This is the canonical pattern for integration with Slack, Telegram, or internal monitoring.
The most commonly used condition in production is "only deploy from the main branch". Do it by comparing the github.ref context, which holds the full ref of the current branch:
jobs:
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- run: ./deploy.sh productionThis way, pushes to other branches (for example staging or feature branches) still run build and test, but will never touch production.
Now let's combine everything into one workflow that reflects a real production pipeline:
name: Deploy Pipeline
on:
push:
branches: [main, staging]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
test:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- run: npm test
deploy:
runs-on: ubuntu-latest
needs: [build, test]
if: github.ref == 'refs/heads/main'
steps:
- run: ./deploy.sh production
notify:
runs-on: ubuntu-latest
needs: [build, test, deploy]
if: failure()
steps:
- run: ./alert.sh "Pipeline gagal"The flow is easy to read: build then test run sequentially, deploy waits for both and only for main, while notify only speaks up when something fails. Notice that needs can accept a list of several jobs at once.
Warning
There's a trap that often surprises beginners: a job that waits (needs) on a job that was cancelled — for example because another job failed and fail-fast is active — will also be cancelled without ever running if: failure(). For notification jobs that must always run regardless of the outcome, use the combination `failure() || cancelled(){:yaml}`. Also remember: never use if: always() for deploy — it will deploy to production even when the build failed.
| Mistake | Symptom | Solution |
|---|---|---|
| Assuming jobs run sequentially | Nondeterministic results | Add needs |
if: always() for deploy | Deploy happens even when build fails | Use failure() or a ref condition |
| Notification not sent when cancelled | Job gets cancelled too | Combine failure() || cancelled() |
| Writing github.ref expressions outside a code block | Workflow error / wrong docs rendering | All curly-brace expressions must be inside a code block |
We've closed the "flow control" chapter of GitHub Actions:
needs — determines execution order and opens up inter-job output access.if + status functions — success(), failure(), always(), cancelled() to execute something only under the desired condition.main, notify only on failure.In the next episode 7, we'll learn about Dynamic Matrix Testing (Matrix Strategy) — a way to test your application across many combinations of operating systems and programming language versions from just one job definition. This is a trick that drastically cuts down your workflow lines!