← Back to blog
Feb 13, 2026

GitHub Actions YAML Workflow Basics: A Practical Guide

Learn the core structure of GitHub Actions YAML workflows, from triggers and jobs to reusable steps, conditions, and deployment-safe patterns for CI/CD teams.

If you have ever opened a workflow file and felt like it looked simple but still hard to reason about, this is the practical baseline.

I will walk through what each part of a workflow does, how to structure jobs safely, and how to wire real runtime updates into the same file.

What a Workflow File Looks Like

Workflow files live in .github/workflows/ and use .yml or .yaml.

name: Backend CI
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: npm ci && npm test

The structure is always the same:

  • name: human-readable workflow name in the GitHub Actions UI.
  • on: trigger rules.
  • jobs: units of work that run on isolated runners.
  • steps: ordered commands/actions inside each job.

Core YAML Blocks You Should Know

jobs:
deploy:
runs-on: ubuntu-latest
env:
NODE_ENV: production
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- name: Build
run: |
npm ci
npm run build

What each field gives you:

  • runs-on: the runner image.
  • uses: a reusable action from GitHub Marketplace or your repo.
  • run: shell commands.
  • with: action inputs.
  • env: shared environment variables for a job or step.

Expressions, Secrets, and Step Outputs

Use GitHub expressions to give each workflow run and rerun its own stream key. Reuse that key at each milestone instead of passing an activity ID between steps.

- name: Start live activity
uses: ActivitySmithHQ/activitysmith-github-action@v1
with:
action: stream_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
stream-key: workflow-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}
payload: |
content_state:
title: "Release Pipeline"
subtitle: "build"
number_of_steps: 3
current_step: 1
type: "segmented_progress"
color: "yellow"
- name: Update live activity
uses: ActivitySmithHQ/activitysmith-github-action@v1
with:
action: stream_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
stream-key: workflow-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}
payload: |
content_state:
title: "Release Pipeline"
subtitle: "deploy"
current_step: 2
type: "segmented_progress"
number_of_steps: 3

Three rules:

  • Store credentials in secrets, not plain YAML.
  • Reuse the same stream key throughout the job.
  • Send complete content state at each milestone.

ActivitySmith operations are informative by default. API or payload errors report ok: false and let the workflow continue. Set errors: true only when an ActivitySmith failure should fail the step.

What You Can Automate in One Workflow

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: "Where to deploy"
required: true
default: "staging"
schedule:
- cron: "0 * * * *"

With this, one file can cover:

  • CI checks for pull requests.
  • Production deploys from main.
  • Manual run buttons for recovery/release tasks.
  • Hourly jobs (health checks, report generation, cleanup).

Control Execution With needs and if

Before looking at the full workflow, these two controls are the ones most teams miss:

  • needs sets job order, so deploy waits for test.
  • if gates a job or step behind explicit conditions.
  • success() and failure() let you split success/failure paths clearly.
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
runs-on: ubuntu-latest
needs: test
if: ${{ github.ref == 'refs/heads/main' && success() }}
steps:
- run: ./deploy.sh
- name: Notify on failure
if: ${{ failure() }}
run: ./notify-failure.sh

Practical End-to-End Example

This example runs tests, deploys on main, streams progress updates, and sends a failure push.

name: API CI and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
concurrency:
group: production-deploy
cancel-in-progress: false
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [22, 24]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm test
deploy:
runs-on: ubuntu-latest
needs: test
if: ${{ github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- name: Start live activity
continue-on-error: true
uses: ActivitySmithHQ/activitysmith-github-action@v1
with:
action: stream_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
stream-key: workflow-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}
payload: |
content_state:
title: "API Deploy"
subtitle: "build"
number_of_steps: 3
current_step: 1
type: "segmented_progress"
color: "yellow"
- name: Build
run: |
npm ci
npm run build
- name: Update live activity
continue-on-error: true
uses: ActivitySmithHQ/activitysmith-github-action@v1
with:
action: stream_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
stream-key: workflow-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}
payload: |
content_state:
title: "API Deploy"
subtitle: "release switch and reload"
current_step: 2
type: "segmented_progress"
number_of_steps: 3
- name: Deploy
run: |
# release directory prep
# artifact upload
# dependency install
# symlink switch
# process reload
- name: End live activity
if: ${{ always() }}
continue-on-error: true
uses: ActivitySmithHQ/activitysmith-github-action@v1
with:
action: end_live_activity_stream
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
stream-key: workflow-${{ github.repository_id }}-${{ github.run_id }}-${{ github.run_attempt }}
payload: |
content_state:
title: "${{ job.status == 'success' && 'API Deploy' || 'API Deploy Failed' }}"
subtitle: "${{ job.status == 'success' && 'done' || 'check the workflow run' }}"
current_step: 3
auto_dismiss_seconds: 10
color: "${{ job.status == 'success' && 'green' || 'red' }}"
- name: Send failed deployment push notification
if: ${{ failure() }}
uses: ActivitySmithHQ/activitysmith-github-action@v1
with:
action: send_push_notification
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
payload: |
title: "Deploy Failed"
message: "Main branch deploy failed. Open GitHub Actions run for details."

Common Mistakes to Avoid

  • Missing id on a step that needs outputs later.
  • Forgetting needs and running deploy in parallel with tests.
  • Letting production deploy runs overlap instead of using a concurrency group with cancel-in-progress: false.
  • Hardcoding environment values that should come from secrets or inputs.
  • Skipping if: ${{ failure() }} branches and losing fast failure visibility.

Final Notes

If you remember only few things, make it these:

  • Keep workflow files small and explicit so intent is obvious during incidents.
  • Treat secrets and step outputs as first-class building blocks.
  • Add failure paths and an always() cleanup step so breakages are visible without leaving stale Live Activities.

Start with a single CI workflow, then expand to deploy and scheduled jobs once the basics are stable.