Skip to main content

Scheduled Automation with GitHub Actions: Real DevOps Use Cases

·7 mins
Table of Contents
Cron triggers in GitHub Actions are powerful for automation that should not need a human to fire it: nightly vulnerability audits, weekly cost checks, certificate expiry alerts. This is the practical guide – real workflows, real use cases, and the traps to avoid.

Most GitHub Actions tutorials cover push and pull request triggers. The schedule trigger gets less attention, but it is often where the most valuable automation lives. A nightly dependency audit that opens an issue when it finds CVEs. A weekly cost check that alerts Slack when your AWS bill spikes. A certificate check that pages you before the cert expires rather than after. None of these need a developer to press a button.

Cron Syntax Quick Reference
#

GitHub Actions uses standard POSIX cron syntax with five fields.

FieldValuesExample
Minute0-5930 = at :30
Hour0-239 = 09:00
Day of month1-311 = first of month
Month1-12 or JAN-DEC* = every month
Day of week0-6 or SUN-SAT (0=Sunday)1 = Monday
# Format:  minute  hour  day-of-month  month  day-of-week
#
# Examples:
# 0 9 * * 1-5      Every weekday at 09:00
# 0 0 * * *        Every day at midnight
# 30 6 * * 1       Every Monday at 06:30
# 0 */4 * * *      Every 4 hours
# 0 9 1 * *        First of every month at 09:00
Important

GitHub Actions runs all scheduled workflows in UTC. There is no way to configure a timezone for the schedule trigger. If your team is in CET (UTC+1) and you want a 09:00 daily run, use 0 8 * * *. During CEST (UTC+2) that becomes 0 7 * * *. Adjust your cron expression seasonally or accept the one-hour drift.

Note

GitHub does not guarantee exact execution times for scheduled workflows. During periods of high GitHub Actions load, runs may be delayed by up to several minutes. Do not use scheduled workflows for anything requiring sub-minute precision.

Combining schedule with workflow_dispatch
#

Always add workflow_dispatch alongside schedule. Without it, you cannot manually trigger the workflow for testing, and you are stuck waiting for the next scheduled run every time you make a change.

.github/workflows/nightly-audit.yaml
name: Nightly Audit

on:
  schedule:
    - cron: '0 2 * * *'   # 02:00 UTC every day
  workflow_dispatch:        # also triggerable manually from the UI or API
    inputs:
      dry_run:
        description: 'Run in dry-run mode (do not open issues)'
        type: boolean
        default: false

workflow_dispatch with inputs lets you parameterize manual runs without creating a separate debug workflow.

Use Case 1: Nightly Dependency Vulnerability Audit
#

Run a vulnerability scanner every night and open a GitHub issue if anything is found, rather than finding out when a developer happens to run npm audit locally.

.github/workflows/nightly-audit.yaml
name: Nightly Dependency Audit

on:
  schedule:
    - cron: '0 2 * * *'
  workflow_dispatch:

jobs:
  audit:
    runs-on: ubuntu-latest
    permissions:
      issues: write
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Run pip-audit
        id: audit
        run: |
          pip install pip-audit
          pip-audit --output json > audit-results.json || echo "VULNERABILITIES_FOUND=true" >> $GITHUB_ENV

      - name: Open issue if vulnerabilities found
        if: env.VULNERABILITIES_FOUND == 'true'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const results = fs.readFileSync('audit-results.json', 'utf8');
            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: `[Security] Dependency vulnerabilities found - ${new Date().toISOString().split('T')[0]}`,
              body: `## Vulnerability Audit Results\n\`\`\`json\n${results}\n\`\`\`\n\nTriggered by nightly audit workflow.`,
              labels: ['security', 'dependencies']
            });

Use Case 2: Weekly AWS Cost Check
#

Query your AWS cost for the past 7 days every Monday morning and post to Slack if spend exceeds a threshold.

.github/workflows/weekly-cost-check.yaml
name: Weekly AWS Cost Check

on:
  schedule:
    - cron: '0 7 * * 1'   # Monday 07:00 UTC
  workflow_dispatch:

jobs:
  cost-check:
    runs-on: ubuntu-latest
    steps:
      - name: Get AWS cost for last 7 days
        id: cost
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: eu-central-1
        run: |
          END=$(date -u +%Y-%m-%d)
          START=$(date -u -d '7 days ago' +%Y-%m-%d)
          AMOUNT=$(aws ce get-cost-and-usage \
            --time-period Start=$START,End=$END \
            --granularity MONTHLY \
            --metrics "UnblendedCost" \
            --query 'ResultsByTime[0].Total.UnblendedCost.Amount' \
            --output text)
          echo "COST=$AMOUNT" >> $GITHUB_ENV
          echo "Weekly spend: $AMOUNT USD"

      - name: Alert Slack if spend exceeds threshold
        if: ${{ env.COST > 500 }}
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
        run: |
          curl -s -X POST "$SLACK_WEBHOOK" \
            -H "Content-Type: application/json" \
            -d "{\"text\": \":warning: AWS weekly spend is \$$COST USD (threshold: \$500). Review the Cost Explorer.\"}"

Use Case 3: SSL Certificate Expiry Check
#

Check your public endpoints’ certificate expiry dates weekly and alert when a certificate is within 30 days of expiring.

.github/workflows/cert-check.yaml
name: Certificate Expiry Check

on:
  schedule:
    - cron: '0 8 * * 1'   # Monday 08:00 UTC
  workflow_dispatch:

jobs:
  cert-check:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        host:
          - api.example.com
          - app.example.com
    steps:
      - name: Check certificate for ${{ matrix.host }}
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
        run: |
          EXPIRY=$(echo | openssl s_client -servername ${{ matrix.host }} \
            -connect ${{ matrix.host }}:443 2>/dev/null \
            | openssl x509 -noout -enddate \
            | cut -d= -f2)
          EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
          NOW_EPOCH=$(date +%s)
          DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
          echo "Certificate for ${{ matrix.host }} expires in $DAYS_LEFT days ($EXPIRY)"
          if [ "$DAYS_LEFT" -lt 30 ]; then
            curl -s -X POST "$SLACK_WEBHOOK" \
              -H "Content-Type: application/json" \
              -d "{\"text\": \":rotating_light: Certificate for *${{ matrix.host }}* expires in *$DAYS_LEFT days* ($EXPIRY). Renew immediately.\"}"
          fi
Tip

Use strategy.matrix to check multiple hosts in parallel without duplicating job definitions. Each matrix entry runs in its own job with its own log.

Use Case 4: Automated Dependency Updates
#

Run npm update weekly, commit the result, and open a pull request automatically if any packages were updated.

.github/workflows/weekly-deps-update.yaml
name: Weekly Dependency Update

on:
  schedule:
    - cron: '0 6 * * 1'   # Monday 06:00 UTC
  workflow_dispatch:

jobs:
  update:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Update dependencies
        run: npm update

      - name: Create pull request if anything changed
        uses: peter-evans/create-pull-request@v6
        with:
          commit-message: 'chore: weekly dependency update'
          title: 'chore: weekly dependency update'
          body: 'Automated weekly dependency update via GitHub Actions.'
          branch: automated/weekly-deps-update
          delete-branch: true
          labels: dependencies

Concurrency Control
#

If a scheduled workflow takes longer than its schedule interval (e.g., a 5-minute workflow on a 2-minute schedule), multiple runs pile up. Use the concurrency key to cancel in-progress runs when a new one starts.

.github/workflows/nightly-audit.yaml
name: Nightly Audit

on:
  schedule:
    - cron: '0 2 * * *'
  workflow_dispatch:

concurrency:
  group: nightly-audit
  cancel-in-progress: true

jobs:
  audit:
    # ...

group is a string that identifies the concurrency group. Any new run in the same group will cancel the currently running one (if cancel-in-progress: true). Use a fixed string for scheduled workflows where you never want two runs overlapping.

Debugging Scheduled Workflows
#

Warning

Scheduled workflows only run on the default branch. If you add a schedule trigger on a feature branch, it will be ignored. Test your workflow by merging to the default branch and using workflow_dispatch to trigger it manually before the next scheduled run.

Useful debugging checklist:

  • Add workflow_dispatch to every scheduled workflow (no exceptions).
  • Check the Actions tab to see if the workflow is listed – if it is not listed, the YAML is invalid or the workflow is on a non-default branch.
  • Use act (the local GitHub Actions runner) for rapid iteration without waiting for the scheduler.
  • Add a step that prints date -u to confirm the UTC time context.
**Forgetting all times are UTC.** A workflow set to `0 9 * * *` runs at 09:00 UTC, which is 10:00 or 11:00 in Central European time depending on daylight saving. Document the UTC time prominently in the workflow file comment. **No `workflow_dispatch` for debugging.** Without it, every change to the workflow requires waiting for the next scheduled run to validate. This extends the iteration cycle from seconds to hours or days. Always add `workflow_dispatch`. **No `concurrency` key on long-running jobs.** Scheduled jobs that overlap create duplicate issues, duplicate Slack alerts, and confusing parallel state. Add a `concurrency` group with `cancel-in-progress: true` as a default. **Putting long-lived secrets directly in cron workflows.** Scheduled workflows run unattended. If a secret is rotated, the workflow silently fails until someone notices missing alerts. Add a step that validates required secrets are non-empty at the start of the workflow, so failures are obvious.

If you want to go deeper on any of this, I offer 1:1 coaching sessions for engineers working on AI integration, cloud architecture, and platform engineering. Book a session (50 EUR / 60 min) or reach out at manuel.fedele+website@gmail.com.

Related