Understanding contexts like github and inputs, managing environment variables with workflow, job, and step scopes, then sending data between steps using GITHUB_ENV and GITHUB_OUTPUT.

In episode 4 you could already fill steps with run and uses. But there's one question that's sure to arise as your workflows become real: how does a workflow "know" things about its context? — who sent the commit, from which branch, what code version, or which API to call. The answer lies in three mechanisms we'll dissect in this episode: contexts, environment variables, and variables.
Understanding these three is the point where your workflows transform from "scripts run by GitHub" into "smart, configurable programs". Without them, you'll be forced to write the same values over and over inside YAML files.
Context is a data structure containing information about the currently running workflow. Each context holds properties you can read, accessed via expressions. The most commonly used contexts:
| Context | Contents | Example property |
|---|---|---|
github | Repository and event metadata | github.actor, github.sha, github.ref |
env | Environment variables already set | env.APP_ENV |
vars | Non-sensitive variables from repo/env settings | vars.API_URL |
job | Information about the running job | job.status |
steps | Outputs produced by a specific step | steps.<id>.outputs.<key> |
runner | Information about the runner used | runner.os, runner.arch |
inputs | Input values from workflow_dispatch | inputs.environment |
secrets | Encrypted secret values | secrets.DEPLOY_KEY |
needs | Outputs of dependent jobs | needs.<job>.outputs.<key> |
Context values are accessed via expressions written inside the workflow. Always within a fenced code block — expressions are never written as free text:
steps:
- name: Tampilkan konteks
run: |
echo "Di-commit oleh ${{ github.actor }}"
echo "Commit SHA : ${{ github.sha }}"
echo "Branch ref : ${{ github.ref }}"When the workflow runs, ${{ github.actor }} is replaced with the username that sent the event, github.sha with the commit hash, and github.ref with the branch or tag reference — for example refs/heads/main. Expressions like this are how workflows make decisions: for example, running deploy only if github.ref points to refs/heads/main.
Environment variables are variables whose values are available to shell commands inside a step. There are three scope levels, and the closest one always wins:
env: at the very top of the file, applies to all jobs.env: inside a job, applies to all steps of that job.env: inside a single step, applies only to that step.name: Env Scoping
on: push
env:
WF_LEVEL: dari-workflow
jobs:
build:
runs-on: ubuntu-latest
env:
JOB_LEVEL: dari-job
steps:
- name: Step dengan env sendiri
env:
STEP_LEVEL: dari-step
run: |
echo "workflow : $WF_LEVEL"
echo "job : $JOB_LEVEL"
echo "step : $STEP_LEVEL"Inside run, environment variables are accessed like regular shell variables with $NAME — unlike contexts which use ${{ ... }}. Also note that the same values can be accessed via the env context in expressions.
Because env vars inside YAML must be committed to the repository, their values aren't suitable for things that change between environments or that you want to change without editing code. For that, GitHub provides variables — name-value pairs stored in the repository settings, not in the code.
How to set them up: open the repository → Settings → Secrets and variables → Actions → Variables, then add for example API_URL with the value https://api.example.com. Inside the workflow:
steps:
- name: Panggil API
run: curl -s "${{ vars.API_URL }}/health"The advantage: values can be changed from the GitHub UI without committing changes, and can differ per environment. vars is suitable for things that are not sensitive — for secrets, always use secrets (covered in depth in episode 9).
Warning
Never store passwords, API keys, or tokens in vars — these variables show up as plain text in logs and can be read by anyone with repository access. Sensitive data belongs only in secrets, which are automatically masked in logs.
There are two official ways to pass data from one step to another:
1. GITHUB_ENV — exports an environment variable that applies to all subsequent steps in the same job.
2. GITHUB_OUTPUT — writes an output bound to a specific step, then read by another step with steps.<id>.outputs.<key>.
A complete example of both:
steps:
- name: Tentukan versi build
id: version
run: |
echo "app_version=1.4.0" >> "$GITHUB_OUTPUT"
echo "BUILD_NUMBER=42" >> "$GITHUB_ENV"
- name: Pakai output step sebelumnya
run: echo "Versi: ${{ steps.version.outputs.app_version }}"
- name: Pakai env antar step
run: echo "Nomor build: $BUILD_NUMBER"The difference: app_version is written to GITHUB_OUTPUT so it can be read by other steps (or other jobs) via the steps context, while BUILD_NUMBER is written to GITHUB_ENV so it's available as a regular environment variable to all subsequent steps. Also notice the id: version attribute on the first step — that id is the key for reading the output.
Tip
Choose based on need: use GITHUB_OUTPUT if the value is meant to be used as an input between jobs (via needs), and use GITHUB_ENV if the value is only needed throughout the same job. Inter-job outputs will be covered in episode 6 about job dependencies.
In real workflows, the three often work together: vars stores the API URL, the github context determines the branch, and GITHUB_ENV carries values between steps:
name: Deploy Staging
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Siapkan info deploy
run: |
echo "DEPLOY_URL=${{ vars.API_URL }}" >> "$GITHUB_ENV"
- name: Tampilkan info
run: |
echo "Branch : ${{ github.ref }}"
echo "Target : $DEPLOY_URL"Notice the combination of three mechanisms in a few lines: vars.API_URL from settings, github.ref from the context, and DEPLOY_URL from the GITHUB_ENV just set by the previous step.
In episode 5 you mastered the value system in workflows:
github, env, vars, job, steps, runner, inputs, and how to access properties like github.actor and github.sha.vars): non-sensitive configuration from repository settings, changeable without a commit.GITHUB_ENV for cross-step env vars, and GITHUB_OUTPUT for step-bound outputs readable between jobs.Now your workflows can talk, store configuration, and share data between steps. In episode 6 we'll discuss more advanced execution control: job dependencies with needs and conditional execution with if — how to make jobs run sequentially, only run when certain conditions are met, and leverage status functions like success() and failure(). See you in episode 6!