November 10, 2026 · 4 min read
YouTube Transcripts in GitHub Actions and Scheduled Jobs
In short
youtube-transcript-api and yt-dlp fail in GitHub Actions runners, cron servers, and Docker hosts on DigitalOcean, Hetzner, Render, or Railway, because YouTube blocks datacenter IPs. Call a transcript API from the job instead.
Scheduled jobs that pull new YouTube transcripts, for a newsletter, a search index, or an AI digest, usually run on a GitHub Actions runner, a VPS, or a container platform. All of them use datacenter IPs: GitHub's hosted runners run on Azure, and DigitalOcean, Hetzner, Linode, OVH, Render, Railway, Fly.io, and Heroku are all well-known ranges. YouTube blocks caption requests from them, so the job fails with RequestBlocked, IpBlocked, or a yt-dlp bot check.
A daily workflow that fetches new transcripts
Add TRANSCRIPTYT_KEY as a repository secret. The job lists a channel's latest uploads and saves each new transcript as a Markdown file in the repo.
# .github/workflows/transcripts.yml
on:
schedule: [{ cron: "0 6 * * *" }]
workflow_dispatch:
jobs:
fetch:
runs-on: ubuntu-latest
permissions: { contents: write }
steps:
- uses: actions/checkout@v4
- run: python fetch.py
env:
TRANSCRIPTYT_KEY: ${{ secrets.TRANSCRIPTYT_KEY }}
- run: |
git config user.name github-actions
git config user.email github-actions@users.noreply.github.com
git add transcripts && git commit -m "New transcripts" || true
git pushThe script
# fetch.py
import os, pathlib, requests
API = "https://transcript-yt.com/v1"
H = {"Authorization": f"Bearer {os.environ['TRANSCRIPTYT_KEY']}"}
out = pathlib.Path("transcripts"); out.mkdir(exist_ok=True)
videos = requests.get(f"{API}/playlist", params={"url": "https://www.youtube.com/@mkbhd", "limit": 10},
headers=H, timeout=60).json()["videos"]
for v in videos:
path = out / f"{v['videoId']}.md"
if path.exists():
continue
r = requests.get(f"{API}/transcript", params={"url": v["videoId"], "format": "md"},
headers=H, timeout=300)
if r.ok:
path.write_text(r.text)Same job on a VPS or container
The script runs unchanged from cron, a Docker container, or a Render or Railway cron job, since only transcript-yt.com is contacted. Without code, the n8n, Make, and Zapier guides build the same schedule.
Common questions
Why does youtube-transcript-api fail in GitHub Actions?
GitHub-hosted runners use Azure datacenter IPs, which YouTube blocks for caption requests. A self-hosted runner on a home connection works, but ties the job to that machine.
Does a VPS on Hetzner or DigitalOcean work?
Usually not for long. VPS providers' IP ranges are known datacenter ranges, and YouTube blocks or rate-limits them, sometimes from the first request.
How do I get new videos from a channel on a schedule?
Call /v1/playlist with the channel URL to list its latest uploads (free on paid plans), keep the IDs you've already processed, and fetch transcripts only for new ones.
How do I store the API key in GitHub Actions?
Add it as a repository secret and pass it to the step as an environment variable: env: TRANSCRIPTYT_KEY: ${{ secrets.TRANSCRIPTYT_KEY }}.