GitHub Actions Scheduled Script Execution

The cron-job.org introduced in the previous section is useful, but it has one prerequisite: you must first have a running backend server (API) for it to ping.

If you only have a simple Python data analysis script (e.g., fetching Bitcoin prices every morning and sending them to a Line group), writing a FastAPI and deploying it just for this small script is overkill.

Here comes the biggest perk for developers! After Microsoft acquired GitHub, it gifted us an incredible tool: GitHub Actions.

What is GitHub Actions?

GitHub Actions was originally designed for CI/CD (automated testing and deployment). When you push code, GitHub "rents" you a high-performance virtual machine (usually Ubuntu) for free to run your tests.

But clever developers discovered that GitHub Actions' triggers aren't limited to "code pushes"—they also support schedule (timed execution)!

This means we can store our code in a GitHub repository and have GitHub's servers execute it on a schedule. For public repositories (Public Repo), this is completely free with unlimited minutes; even private repositories (Private Repo) get 2000 free minutes per month!

Hands-on Tutorial: Running a Python Web Scraper Daily

Let’s start with a simple Python script. Assume your project folder looks like this:

my-auto-script/
├── main.py
├── requirements.txt
└── .github/
    └── workflows/
        └── cron.yml

1. Write Your Python Script (main.py)

This script fetches data from an API and prints it:

# main.py
import requests
from datetime import datetime

def job():
    print(f"[{datetime.now()}] Script started! Fetching data...")
    
    # Add your scraping logic or Line message logic here
    response = requests.get("https://api.coindesk.com/v1/bpi/currentprice.json")
    data = response.json()
    
    btc_price = data['bpi']['USD']['rate']
    print(f"Current Bitcoin price: {btc_price} USD")
    print("Task completed!")

if __name__ == "__main__":
    job()

If external packages (e.g., requests) are used, create a requirements.txt:

requests==2.31.0

2. Create the GitHub Actions Configuration File

This is the most critical step. Create a .github/workflows/cron.yml file in the project root. Note: The folder name must be exact (.github/workflows/).

Paste the following into cron.yml:

name: Daily Bitcoin Price Checker

# Set trigger conditions
on:
  # Allows manual execution via the GitHub UI (for debugging)
  workflow_dispatch:
  # Set the schedule (uses UTC time!)
  schedule:
    # Runs daily at 8:00 AM (Taiwan time)
    # Note: Taiwan is UTC+8, so use 00:00 UTC here
    - cron: '0 0 * * *'

jobs:
  run-script:
    # Specify the OS for the virtual machine
    runs-on: ubuntu-latest
    
    steps:
      # Step 1: Download the code from GitHub to the VM
      - name: Checkout code
        uses: actions/checkout@v4

      # Step 2: Set up Python in the VM
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          
      # Step 3: Install dependencies
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
          
      # Step 4: Execute the Python script!
      - name: Run my python script
        run: python main.py
        
      # (Optional) If the script needs environment variables (e.g., API Key):
      # env:
      #   LINE_TOKEN: ${{ secrets.LINE_TOKEN }}

[!WARNING] Scheduling Pitfall: Timezone Issues GitHub Actions servers use UTC (Coordinated Universal Time). Taiwan time is UTC+8. If you want the script to run at 8:00 AM Taiwan time, the crontab must subtract 8 hours, i.e., 0 0 * * * (midnight UTC). Many beginners stumble here!

3. Upload to GitHub

After committing and pushing the code to GitHub, click the "Actions" tab at the top of your repository page.

  1. In the left sidebar, you’ll see the workflow named Daily Bitcoin Price Checker.
  2. Since we included workflow_dispatch, you can click "Run workflow" to manually test it.
  3. Check the execution logs to see the VM booting, Python installing, and your script running successfully!

Limitations and Workarounds for GitHub Actions Scheduling

GitHub Actions is powerful but has a few limitations:

  1. Timing Inaccuracy: Due to shared server resources globally, scheduled jobs (e.g., 0 0 * * *) may be delayed by 5–15 minutes during peak hours. If your task requires "precise second-level timing," avoid Actions.
    • Workaround: Avoid the top of the hour! Use 17 0 * * * (UTC 0:17) to bypass congestion.
  2. Free Tier Limits: Private repositories get 2000 free minutes per month. For a daily 1-minute script, this is more than enough (only ~30 minutes/month).

Mastering GitHub Actions scheduling gives you a free, maintenance-free execution host—the ultimate secret weapon for building micro-SaaS or automation tools!

Workflow Syntax Reference

GitHub Actions uses POSIX cron syntax in the schedule event.

Schedule Event Structure

name: Scheduled Task
on:
  schedule:
    - cron: '0 8 * * *'   # Daily at 8:00 UTC
    - cron: '0 0 * * 0'   # Weekly at midnight Sunday

jobs:
  run-task:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run scheduled-task

Multiple Schedules in One Workflow

on:
  schedule:
    - cron: '*/10 * * * *'   # Every 10 minutes (keep-alive)
    - cron: '0 9 * * 1-5'     # Weekdays at 9 AM (business report)
    - cron: '0 0 1 * *'       # 1st of month (billing)

Common Workflow Patterns

API Ping Workflow

name: Keep Alive
on:
  schedule:
    - cron: '*/10 * * * *'
jobs:
  ping:
    runs-on: ubuntu-latest
    steps:
      - name: Ping API
        run: |
          curl -s -o /dev/null -w "%{http_code}" \
            https://my-app.onrender.com/api/health

Data Backup Workflow

name: Daily Backup
on:
  schedule:
    - cron: '0 3 * * *'
jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Generate Backup
        run: |
          curl -s -o backup.json \
            -H "Authorization: Bearer ${{ secrets.API_KEY }}" \
            https://api.example.com/export
      
      - name: Upload to S3
        uses: jakejarvis/s3-sync-action@master
        with:
          args: --acl private
        env:
          AWS_S3_BUCKET: ${{ secrets.AWS_BUCKET }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET }}
          SOURCE_DIR: ./backup.json

Database Cleanup Workflow

name: Cleanup Expired Data
on:
  schedule:
    - cron: '0 4 * * 0'   # Every Sunday at 4 AM
jobs:
  cleanup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
      
      - name: Install Dependencies
        run: pip install requests
      
      - name: Run Cleanup
        env:
          DB_URL: ${{ secrets.DATABASE_URL }}
          CLEANUP_TOKEN: ${{ secrets.CRON_SECRET }}
        run: python scripts/cleanup.py

Secrets Management

Never hardcode credentials in workflow files. Use GitHub Secrets.

Setting Secrets

  1. Go to repo: SettingsSecrets and variablesActions
  2. Click New repository secret
  3. Add your sensitive values

Accessing Secrets

steps:
  - name: Call API
    env:
      API_KEY: ${{ secrets.API_KEY }}
      DATABASE_URL: ${{ secrets.DATABASE_URL }}
      WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}
    run: |
      curl -X POST $WEBHOOK_URL \
        -H "Authorization: Bearer $API_KEY" \
        -d '{"event": "scheduled-task"}'

Monitoring and Debugging

Viewing Workflow Runs

  1. Go to your GitHub repository
  2. Click Actions tab
  3. Select the workflow from the left sidebar
  4. Click a specific run to see logs

Adding Debug Logging

steps:
  - name: Run task with logging
    run: |
      echo "Starting scheduled task at $(date -u '+%Y-%m-%d %H:%M:%S')"
      # Your task logic here
      echo "Task completed with exit code: $?"

Notifications on Failure

name: Critical Task
on:
  schedule:
    - cron: '0 * * * *'
jobs:
  task:
    runs-on: ubuntu-latest
    steps:
      - run: ./run-critical-task.sh
  alert:
    needs: task
    if: failure()
    runs-on: ubuntu-latest
    steps:
      - name: Send Alert
        run: |
          curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
            -H 'Content-Type: application/json' \
            -d '{"text": "Scheduled task FAILED!"}'

Pricing and Limits

| Plan | Free Minutes | Max Job Time | Artifacts | |------|-------------|-------------|-----------| | Public repo | Unlimited | 6 hours | Unlimited | | Private (Free) | 2,000 min/mo | 6 hours | 500 MB | | Team | 3,000 min/mo | 6 hours | 500 MB | | Enterprise | 50,000 min/mo | 6 hours | 5 GB |

Summary

GitHub Actions provides powerful cron scheduling integrated directly with your repositories. It's free for public repos and includes 2,000 free minutes per month for private repos.

Key takeaways:

  • Use on: schedule with cron syntax in workflow YAML |
  • Multiple cron schedules can run in a single workflow |
  • Store secrets in GitHub Secrets, never hardcode values |
  • Monitor runs in the Actions tab with full logs |
  • 2,000 free minutes/month for private repos, unlimited for public |
  • Use if: failure() for alerting on failed tasks |
  • Avoid peak hours (top of UTC hour) to reduce delay |
  • Combine with API endpoints for complex automation |

What's Next: Vercel Cron Jobs

The next chapter covers Vercel Cron Jobs for serverless Next.js scheduled tasks.

Unlock Full Tutorial

This chapter is paid content. Join the project to unlock over 5000 words of deep analysis, including 10+ god-tier Prompts and real Source Code examples!