Skip to content

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!"
  1. Open the repository → Actions tab → set up a workflow yourself (or Add file → Create new file under Code).
  2. Set the path to .github/workflows/first-action.yml — typing the / characters creates the directories.
  3. Paste the YAML, then Start commit → commit to main.
  4. 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
# Run a workflow_dispatch workflow without opening the browser
gh workflow run first-action.yml --ref main

# Watch the latest run
gh run watch

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_dispatch only. Add push / pull_request triggers after the first successful manual run.
  • Keep workflow_dispatch permanently in the trigger list — re-running a pipeline by hand is invaluable during an incident.
  • Name every step. Run npm test in the log beats Run npm test collapsed 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 run lines.
  • Set an explicit least-privilege token scope even on a trivial workflow:
    permissions:
      contents: read
    
  • 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_request triggers 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_dispatch is 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 — singular workflow, 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 - before name: in steps. steps: takes a list, not a mapping.
  • No "Run workflow" button. The workflow is not on the default branch yet, or workflow_dispatch is missing from on:.
  • Expecting repository files to be present. These example steps only echo text; any step touching your code needs actions/checkout first.
  • Unquoted special characters. A run value 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, and jobs.
  • A job needs runs-on plus a list of steps.
  • run: executes a shell command; uses: pulls in an action.
  • on: workflow_dispatch gives 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 run scripts, using Marketplace actions, and workflow run logs and artifacts.