1.02 Creating Your First GitHub Actions Workflow
Overview
A workflow is just a YAML file committed to .github/workflows/ in a repository. Once it is there,
GitHub reads it automatically and makes it runnable from the Actions tab.
Abstract
Workflow files live at .github/workflows/<name>.yml on a branch (usually main). A minimal
workflow needs three keys: name, on (the triggering event), and jobs. Each job declares a
runs-on runner and a list of steps. Using on: workflow_dispatch makes the workflow
manually triggerable, which is the safest way to test a new pipeline before wiring it to pushes.
Why It Matters in Production
The first workflow you write is the template everyone on the team copies. Getting the basics right — correct path, explicit trigger, named steps — is what makes later pipelines debuggable.
Starting with a manual trigger also matters operationally: a workflow bound to on: push runs the
moment you commit it, before you have verified the YAML, the runner, or the commands.
Key Concepts
| Element | Purpose |
|---|---|
.github/workflows/ |
The only directory GitHub scans for workflow files |
name |
Display name shown in the Actions tab (defaults to the file path if omitted) |
on |
The event(s) that trigger the run |
workflow_dispatch |
Manual trigger — adds a "Run workflow" button in the Actions tab |
jobs |
Map of job IDs (e.g. first-job) to job definitions |
runs-on |
The runner image, e.g. ubuntu-latest |
steps |
Ordered list of units of work, each a run command or a uses action |
name (on a step) |
Label shown in the run log — makes failures readable |
Note
The job key (first-job) is an identifier used by needs and status checks. The name: field
is only for display. Job IDs must be unique within the workflow.
Common Use Cases
- A "Hello World" workflow to confirm Actions is enabled and runners are available
- Manually triggered utility jobs: cache clear, smoke test, one-off migration
- A scratch workflow for testing YAML syntax and runner behavior before adding real steps
- Onboarding a new repository to CI, starting from a GitHub starter template
Example Configuration or Commands
Minimal manually triggered workflow — the smallest thing that actually runs:
# .github/workflows/first-action.yml
name: First Workflow
on: workflow_dispatch
jobs:
first-job:
runs-on: ubuntu-latest
steps:
- name: Print greeting
run: echo "Hello World!"
- name: Print goodbye
run: echo "Done - bye!"
- Open the repository → Actions tab → set up a workflow yourself (or Add file → Create new file under Code).
- Set the path to
.github/workflows/first-action.yml— typing the/characters creates the directories. - Paste the YAML, then Start commit → commit to
main. - Go to Actions → select First Workflow → Run workflow.
mkdir -p .github/workflows
cat > .github/workflows/first-action.yml <<'EOF'
name: First Workflow
on: workflow_dispatch
jobs:
first-job:
runs-on: ubuntu-latest
steps:
- name: Print greeting
run: echo "Hello World!"
EOF
git add .github/workflows/first-action.yml
git commit -m "Add first workflow"
git push origin main
The default starter template GitHub offers looks like this — note it triggers on push and pull
request to main, plus manual dispatch:
name: CI
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run a one-line script
run: echo Hello, world!
Tip
The editor's right-hand Marketplace panel searches published actions and pastes the correct
uses: snippet, while Documentation links the workflow syntax reference. Use it instead of
guessing action names and versions.
Best Practices
Best Practices
- Start every new workflow with
on: workflow_dispatchonly. Addpush/pull_requesttriggers after the first successful manual run. - Keep
workflow_dispatchpermanently in the trigger list — re-running a pipeline by hand is invaluable during an incident. - Name every step.
Run npm testin the log beatsRun npm testcollapsed under an unnamed shell step. - Use 2-space indentation and no tabs — YAML rejects tabs outright.
- Give the file a descriptive name (
ci.yml,deploy-staging.yml); the file name shows in status checks and branch protection rules. - Commit the workflow to the default branch. Manual dispatch only appears once the file exists
on
main(or whatever the default branch is).
Security Best Practices
Security
- Remember the workflow file is source code in the repository. Anything pasted into it is
visible to everyone with read access — never put tokens or endpoints in
runlines. - Set an explicit least-privilege token scope even on a trivial workflow:
- Pin any action you add from the Marketplace to a full commit SHA, and check the publisher — a verified badge indicates the owner, not that the code is safe.
- On a public repository, keep
pull_requesttriggers on GitHub-hosted runners only. Self-hosted runners would execute untrusted fork code on your infrastructure. - Restrict who can dispatch sensitive workflows using an Environment with required reviewers;
workflow_dispatchis available to anyone with write access by default.
Do and Don't
| ✅ Do | ❌ Don't |
|---|---|
Put files in .github/workflows/ |
Use .github/workflow/ or the repo root |
Test with workflow_dispatch first |
Wire a brand-new workflow to on: push |
| Indent with 2 spaces | Mix tabs into YAML |
| Commit the workflow to the default branch | Expect the Run button on a feature branch only |
Declare permissions explicitly |
Leave the default broad token scope |
| Name steps clearly | Ship a wall of unnamed run lines |
Common Mistakes
Common Mistakes
- Wrong path.
.github/workflows/is exact — singularworkflow, a typo, or a nested subfolder means GitHub silently ignores the file. - Missing
runs-on. Every job needs a runner; without it the job fails before any step. - Forgetting the
-beforename:in steps.steps:takes a list, not a mapping. - No "Run workflow" button. The workflow is not on the default branch yet, or
workflow_dispatchis missing fromon:. - Expecting repository files to be present. These example steps only echo text; any step
touching your code needs
actions/checkoutfirst. - Unquoted special characters. A
runvalue containing:or#needs quoting.
Troubleshooting
# Confirm the file is on the default branch at the right path
git ls-tree -r main --name-only | grep '.github/workflows'
# Validate YAML before pushing
yq '.' .github/workflows/first-action.yml > /dev/null && echo "valid YAML"
# List workflows GitHub has actually registered
gh workflow list
# View the last run's logs, including the echo output
gh run view --log
| Symptom | Fix |
|---|---|
| Workflow missing from the Actions tab | Wrong directory path, invalid YAML, or not on the default branch |
| "Run workflow" button absent | Add workflow_dispatch to on: and commit to the default branch |
| "Invalid workflow file" annotation | Read the error's line number — usually indentation or a missing - |
| Run queued forever | runs-on label matches no available runner |
| Actions tab missing entirely | Actions disabled in Settings → Actions → General |
Breaks in production
Editing a workflow directly on main in the browser means the next push-triggered run uses
untested YAML. Once a workflow does real work, change it via a pull request.
Quick Recap
- Workflow files go in
.github/workflows/and are picked up automatically. - The three required keys are
name,on, andjobs. - A job needs
runs-onplus a list ofsteps. run:executes a shell command;uses:pulls in an action.on: workflow_dispatchgives you a manual Run button in the Actions tab.- The workflow must exist on the default branch before it can be dispatched.
Interview / Revision Notes
Quick revision
Q: Where must workflow files be stored?
.github/workflows/ — any .yml/.yaml file there.
Q: What are the minimum keys in a workflow file?
on and jobs; name is optional but recommended.
Q: How do you make a workflow manually runnable?
Add the workflow_dispatch event, and commit it to the default branch.
Q: Difference between run and uses in a step?
run executes a shell command on the runner; uses invokes a reusable action.
Q: What does runs-on do?
Selects the runner (OS/machine) that executes the job's steps.
Q: Difference between a job's key and its name?
The key is the identifier used by needs and status checks; name is display text only.
This topic can be expanded into sub-pages: workflow events and filters, multi-line
runscripts, using Marketplace actions, and workflow run logs and artifacts.