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.
- In the left sidebar, you’ll see the workflow named
Daily Bitcoin Price Checker. - Since we included
workflow_dispatch, you can click "Run workflow" to manually test it. - 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:
- 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.
- Workaround: Avoid the top of the hour! Use
- 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
- Go to repo: Settings → Secrets and variables → Actions
- Click New repository secret
- 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
- Go to your GitHub repository
- Click Actions tab
- Select the workflow from the left sidebar
- 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: schedulewith 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.