TL;DR: A workflow is a YAML file triggered by an event; it contains jobs that run in parallel on separate runners; each job is steps that run in order on one machine. Jobs share no filesystem, so anything passing between them goes through artifacts or a cache.
How to approach it
Give the hierarchy, then immediately state the isolation rule, because that is where beginners lose time. A build job producing a file and a deploy job not finding it is the first thing everybody hits. Then mention the security question, since interviewers ask it and it is what separates a casual user from someone who has run this for a team.
A strong answer
Workflow. A YAML file in .github/workflows/, triggered by an event: push, pull_request, schedule, workflow_dispatch for a manual run, or workflow_call to be invoked by another workflow. One repository can have many, and they are independent.
Job. A unit that runs on one runner. Jobs in a workflow run in parallel by default; needs: creates a dependency so one waits for another. Each job gets a clean machine.
Step. Runs in order within a job, on that job's machine. A step either runs a shell command with run: or invokes a reusable action with uses:.
Runner. The machine. GitHub-hosted runners are clean VMs per job, which is convenient and metered by the minute. Self-hosted runners are yours, which is what you move to for specific hardware, private network access, or when the bill gets uncomfortable.
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test
publish:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./scripts/build-and-push.sh
The isolation rule and what follows from it: jobs share nothing. test and publish are different machines with different disks. Passing a build output between them means actions/upload-artifact then actions/download-artifact, and speeding up dependency installs across runs means actions/cache keyed on a lockfile hash. Even the checkout has to be repeated, which surprises people, and is why a job doing nothing but deploying still starts with actions/checkout.
Two more pieces worth naming. A matrix runs the same job across combinations (node-version: [18, 20, 22]) in parallel, which is how you test several versions without duplicating YAML. And environments give a deployment target a name, with optional required reviewers and its own secrets, which is the mechanism for a production approval gate.
On security, because it gets asked: pin third-party actions to a full commit SHA rather than a tag, since a tag is mutable and uses: some/action@v1 means running whatever that tag points at today. Use OIDC to get short-lived cloud credentials rather than storing long-lived keys as secrets, which is the same workload identity idea Kubernetes uses: the runner proves who it is to the cloud and receives a credential that expires, so there is nothing long-lived to leak. And be careful with pull_request_target, which runs with repository secrets available in the context of a pull request from a fork, which is exactly the shape of a credential-theft bug.
What interviewers probe next
"How do you avoid copying the same workflow into thirty repositories?" Reusable workflows called with workflow_call, or a composite action for a step sequence. Centralising it also gives you one place to fix a security issue.
"When would you use self-hosted runners?" Specific hardware, access to a private network, large caches that are expensive to rebuild, or cost at volume. The trade is that you now own an autoscaling fleet and its security boundary, and a self-hosted runner on a public repository is a serious risk.
"What is the difference between a secret and a variable?" Secrets are encrypted, masked in logs, and unavailable to workflows triggered from forks. Variables are plain configuration. Putting a token in a variable exposes it in the log.
Common mistakes
Expecting files to persist between jobs, then debugging a missing build output that was on another machine.
Referring to third-party actions by tag, so a repository you do not control decides what runs with your secrets.
Running the full matrix on every push instead of on pull requests and main, which burns minutes and money.
Storing long-lived cloud keys as secrets when OIDC will issue short-lived ones.