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 CIon: push: branches: [main]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - run: npm ci && npm testThe structure is always the same:
name: human-readable workflow name in theGitHub ActionsUI.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 buildWhat each field gives you:
runs-on: the runner image.uses: a reusable action fromGitHub Marketplaceor 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: 3Three 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:
needssets job order, so deploy waits for test.ifgates a job or step behind explicit conditions.success()andfailure()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.shPractical End-to-End Example
This example runs tests, deploys on main, streams progress updates, and sends a failure push.
name: API CI and Deployon: push: branches: [main] pull_request: branches: [main] workflow_dispatch:concurrency: group: production-deploy cancel-in-progress: falsejobs: 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
idon a step that needs outputs later. - Forgetting
needsand 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
secretsorinputs. - 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
secretsand 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.
