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.

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@v4
- 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@v4
- uses: actions/setup-node@v4
with:
node-version:node-version:node-version: 22
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

You can stitch steps together with expressions and outputs.

- name: Start live activity
id: start_activity
uses: ActivitySmithHQ/[email protected]
with:
action: start_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
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/[email protected]
with:
action: update_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
live-activity-id: ${{ steps.start_activity.outputs.live_activity_id }}
payload: |
content_state:
title: "Release Pipeline"
subtitle: "deploy"
current_step: 2

Three rules:

  • Store credentials in secrets, not plain YAML.
  • Give important steps an id when you need outputs later.
  • Use steps.<id>.outputs.<name> for wiring state across steps.

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:

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version:node-version: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@v4
- uses: actions/setup-node@v4
with:
node-version:node-version:node-version: 22
cache: npm

- name: Start live activity
id: start_activity
continue-on-error: true
uses: ActivitySmithHQ/[email protected]
with:
action: start_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
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
if: ${{ steps.start_activity.outputs.live_activity_id != '' }}
continue-on-error: true
uses: ActivitySmithHQ/[email protected]
with:
action: update_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
live-activity-id: ${{ steps.start_activity.outputs.live_activity_id }}
payload: |
content_state:
title: "API Deploy"
subtitle: "release switch and reload"
current_step: 2

- name: Deploy
run: |
# release directory prep
# artifact upload
# dependency install
# symlink switch
# process reload

- name: End live activity
if: ${{ steps.start_activity.outputs.live_activity_id != '' }}
continue-on-error: true
uses: ActivitySmithHQ/[email protected]
with:
action: end_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
live-activity-id: ${{ steps.start_activity.outputs.live_activity_id }}
payload: |
content_state:
title: "API Deploy"
subtitle: "done"
current_step: 3

- name: Send failed deployment push notification
if: ${{ failure() }}
uses: ActivitySmithHQ/[email protected]
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.
  • 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 (if: ${{ failure() }}) so breakages are visible immediately.

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

Feb 9, 2026

How I Use ActivitySmith While Deploying ActivitySmith

Deployment progress shown across three iPhone Live Activity stages

I am a huge fan of dogfooding. If I build developer tooling, I want to use it in my own day-to-day workflows.

Deployments are one of the biggest ways I use ActivitySmith: push to main, GitHub Actions runs in the background, and I get step-by-step progress updates on my iPhone lock screen using iOS Live Activities.

The Problem I Kept Running Into

My deployments are automated with GitHub Actions and are fairly simple, but they still have multiple steps and usually take at least a minute. In larger projects and teams, deployment runs often take much longer.

Before ActivitySmith, that meant opening GitHub, finding the repo, going to Actions, opening the current run, and watching it progress until it finished so I knew when I could try it out.

That workflow was annoying. With ActivitySmith, the progress streams directly to my iPhone lock screen in real time, and I know exactly when a release is finished and ready to try.

ActivitySmith deployment live activity completion

What ActivitySmith Does in My Deployments

For every production deployment, ActivitySmith:

  • Starts a Live Activity at the beginning of the workflow.
  • Updates each phase of deployment progress in real time.
  • Ends the activity when production is fully switched.

This is not limited to one backend service. I use the same pattern across various backend services, frontend apps, landing pages, and other projects.

My GitHub Actions Workflow

Here is the workflow I use in production:

name: Deploy API to Production

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v4
with:
node-version:node-version:node-version: 24
cache: npm

- name: Start live activity
id: start_activity
continue-on-error: true
uses: ActivitySmithHQ/[email protected]
with:
action: start_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
payload: |
content_state:
title: "API Deployment"
subtitle: "ci: install & build"
number_of_steps: 4
current_step: 1
type: "segmented_progress"
color: "yellow"

- name: Build
run: |
npm ci
npm run build

- name: Update live activity
if: ${{ steps.start_activity.outputs.live_activity_id != '' }}
continue-on-error: true
uses: ActivitySmithHQ/[email protected]
with:
action: update_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
live-activity-id: ${{ steps.start_activity.outputs.live_activity_id }}
payload: |
content_state:
title: "ActivitySmith API Deployment"
subtitle: "prepare release & upload"
current_step: 2

- name: Set release id
run: |
echo "RELEASE_ID=$(date +%Y%m%d%H%M%S)" >> $GITHUB_ENV

- name: Prepare release dir on remote server
run: |
ssh -o StrictHostKeyChecking=no [email protected] "RELEASE_ID=${RELEASE_ID} bash -s" << 'EOF'
set -euo pipefail
BASE="/srv/apps/activitysmith-api"
NEW="$BASE/releases/$RELEASE_ID"
mkdir -p "$BASE/releases"
rm -rf "$NEW"
mkdir -p "$NEW"
EOF

- name: Upload built artifacts to remote server
run: |
# rsync release artifacts to the new server release directory

- name: Update live activity
if: ${{ steps.start_activity.outputs.live_activity_id != '' }}
continue-on-error: true
uses: ActivitySmithHQ/[email protected]
with:
action: update_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
live-activity-id: ${{ steps.start_activity.outputs.live_activity_id }}
payload: |
content_state:
title: "ActivitySmith API Deployment"
subtitle: "prod install & reload"
current_step: 3

- name: Install prod deps, switch release, and reload
run: |
# Remote deployment script:
# - Install production dependencies in the new release directory
# - Atomically switch current symlink to the new release
# - Gracefully reload PM2 cluster behind nginx
# - Prune old releases while keeping rollback-safe history

- name: End live activity
if: ${{ steps.start_activity.outputs.live_activity_id != '' }}
continue-on-error: true
uses: ActivitySmithHQ/[email protected]
with:
action: end_live_activity
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
live-activity-id: ${{ steps.start_activity.outputs.live_activity_id }}
payload: |
content_state:
title: "ActivitySmith API Deployment"
subtitle: "done"
current_step: 4

Push Notification

ActivitySmith push notification

You can also use ActivitySmith to send push notifications to your iOS devices.

- name: Send deployment complete push notification
uses: ActivitySmithHQ/[email protected]
with:
action: send_push_notification
api-key: ${{ secrets.ACTIVITYSMITH_API_KEY }}
payload: |
title: "Build Failed 🚨"
message: "CI pipeline failed on main branch"

Why Zero Downtime Is Non-Negotiable

ActivitySmith is API infrastructure. Users expect near-constant availability.

Before I implemented zero-downtime deployments, the Live Activity lifecycle would break during release restarts because the API went down mid-deploy, so updates could not be delivered.

Deployments now run behind nginx with PM2 in cluster mode using zero-downtime reloads (worker A, then worker B), so traffic stays up and Live Activity updates continue reliably.

More Than One Project

This pattern now powers every deployment workflow I care about, not just ActivitySmith core services. Once wired in, every release gives real-time confidence without babysitting logs.

And outside GitHub Actions, the same approach works from scripts, cron jobs, workers, agents and more. Basically any backend. Zapier and n8n integrations are complete and currently pending marketplace approval.

Try It on Your Next Deployment