How to Capture Screenshots of a Website on a Schedule
Capture a website automatically with Playwright and GitHub Actions. Choose a reliable wait condition, schedule runs, and keep screenshots where you need them.
To capture a website on a schedule, write a browser script that opens the page and saves a screenshot, then have a scheduler run that script at the interval you choose. Playwright handles the browser capture; GitHub Actions is one option for recurring runs. These are separate jobs: the browser controls what gets captured, while the scheduler controls when it runs.
The guide below sets up a runnable Playwright script and a GitHub Actions workflow. It covers viewport and full-page images, waits for dynamic content, output retention, reliability, troubleshooting, and a managed option for avoiding browser-runtime maintenance.
1. Decide what the scheduled capture should record
Before choosing a schedule, define the screenshot’s purpose. A viewport image records what fits in the browser window. A full-page image records the scrollable document and can be much taller. Use the same viewport, browser, format, and page state on every run if you plan to compare images over time.
| Decision | Options | Practical guidance |
|---|---|---|
| Image scope | Viewport or full page | Use viewport for a consistent first-screen record. Use full page when lower-page content matters; long pages produce large images. |
| Page readiness | Load event, selector, or delay | Prefer a selector or other page-specific condition for asynchronous content. Use a fixed delay only when there is no reliable condition. |
| Browser size | Viewport width and height | Set dimensions explicitly so layout changes do not come from different defaults. |
| Output | PNG, JPEG, or WebP | Choose a format supported by your capture method and downstream use. PNG is useful for exact visual comparison; compressed formats reduce storage. |
| Persistence | Repository, workflow artifact, or external storage | Pick a destination and retention period before creating a long-running schedule. |
2. Create a Playwright capture script
This Node.js example captures a page and writes screenshot.png. It uses a fixed viewport and waits for the page’s load event before taking the image.
import { chromium } from 'playwright';
const url = process.env.TARGET_URL ?? 'https://example.com';
const output = process.env.OUTPUT_PATH ?? 'screenshot.png';
const fullPage = process.env.FULL_PAGE === 'true';
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1
});
const response = await page.goto(url, {
waitUntil: 'load',
timeout: 60_000
});
if (!response || !response.ok()) {
throw new Error(`Navigation failed: HTTP ${response?.status() ?? 'no response'}`);
}
await page.screenshot({ path: output, fullPage, type: 'png' });
console.log(`Saved ${output}`);
} finally {
await browser.close();
}
Install Playwright and its Chromium browser locally, then run the script:
npm init -y
npm install playwright
npx playwright install chromium
node capture.mjs
Save the script as capture.mjs. Set TARGET_URL, OUTPUT_PATH, or FULL_PAGE=true in the environment to change the target, destination, or image scope. The script checks the navigation response so a failed HTTP request does not silently become a successful scheduled capture.
For pages whose important content is rendered after navigation, wait for a page-specific selector before taking the image:
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.locator('[data-ready="true"]').waitFor({ state: 'visible', timeout: 30_000 });
await page.screenshot({ path: output, fullPage, type: 'png' });
Replace the selector with one that indicates the content you need is ready. If the page has no stable readiness signal, a deliberate short delay can help, but it may waste time on fast runs and still be too short on slow ones.
3. Add a GitHub Actions schedule
Create .github/workflows/capture.yml on the repository’s default branch. This example runs daily at 07:17 in New York time and can also be started manually:
name: Scheduled website screenshot
on:
schedule:
- cron: '17 7 * * *'
timezone: 'America/New_York'
workflow_dispatch:
jobs:
capture:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Capture page
run: node capture.mjs
env:
TARGET_URL: https://example.com
OUTPUT_PATH: screenshot.png
FULL_PAGE: 'false'
- name: Save screenshot artifact
uses: actions/upload-artifact@v4
with:
name: scheduled-screenshot
path: screenshot.png
retention-days: 14
Commit package.json and package-lock.json so npm ci has a lockfile. GitHub Actions uses UTC by default; the timezone field lets you specify an IANA timezone. A scheduled run uses the workflow file on the default branch and the latest commit there.
GitHub documents a five-minute minimum schedule interval, but that is not an execution-time guarantee. Scheduled runs can be delayed or dropped under high load, especially near the start of an hour. This example schedules at minute 17 to avoid that particularly busy minute. For daylight-saving changes, GitHub says a scheduled time in a skipped spring-forward hour advances to the next valid time. Public repositories can have scheduled workflows disabled after 60 days without repository activity. See GitHub’s schedule event documentation for current platform behavior.
4. Choose where screenshots live
The workflow above uploads each image as an artifact and keeps it for 14 days. That is useful for short-term inspection; set retention to match how long you need the record. Other common choices are:
- Workflow artifacts: convenient for reviewing recent runs; set an appropriate retention period.
- Repository commits: easy to browse with source history, but repeated binary images can grow repository size. The shot-scraper GitHub Actions guide demonstrates committing generated screenshots back to a repository.
- External storage: useful for longer retention or larger collections. Configure credentials as repository secrets and confirm the storage provider’s retention, access, and cost terms.
Do not commit credentials or private page contents to a public repository. If the captured page requires authentication, use a supported session approach and store secrets in the CI platform’s secret store. Confirm that the resulting screenshot itself is safe to retain and share.
5. Tune waits and capture behavior
A screenshot is only useful if it captures the intended page state. Playwright supports viewport screenshots, full-page captures, and returning image bytes for processing instead of writing directly to a file. Use the capture options documented in the Playwright screenshot guide.
- Wait for navigation:
page.goto()can wait for a selected lifecycle event. The example usesload; for pages that keep connections open, a less strict event plus a page-specific selector may be more suitable. - Wait for content: use a locator wait for the element that matters. Avoid assuming one delay works for every run.
- Wait with shot-scraper: its CLI supports
--waitfor a fixed pause and--wait-forfor a JavaScript expression. The expression wait times out after 30 seconds according to its documentation. - Choose full page carefully: lazy-loaded images and content may not appear until scrolled into view. Check the resulting image and use page-specific preparation when needed.
- Keep capture conditions stable: control viewport, device scale, locale-dependent page state, and any consent or login interactions that affect what the visitor sees.
The shot-scraper project also documents GitHub Actions workflows, making it a command-line alternative when a CLI fits your existing workflow. See its project documentation for usage details.
6. Reliability, performance, and cost
Reliability
GitHub Actions is convenient for periodic jobs, but its documented schedule behavior is best-effort during high load. If a missed capture matters, send failure notifications and monitor for missing outputs with a separate check. A workflow that completed without an error is not proof that the screenshot depicts the expected content, so keep an eye on output dimensions and page state.
Performance
Browser startup, page load, and image rendering dominate a simple capture. Reuse a page-specific readiness condition instead of adding a long delay to every run. Full-page images take more time and memory on very long documents. If you capture many URLs, process them in controlled batches rather than launching an unbounded number of browsers at once.
Cost
The self-managed approach uses your CI minutes and whatever storage you choose. Browser installation and execution add work to each workflow run, while artifact retention or repository growth affects how much output you keep. Calculate the schedule frequency, number of URLs, average run duration, and retention period against your current CI and storage limits. A managed screenshot API moves browser-runtime maintenance to a service, but its current pricing, scheduling support, retention, authentication options, and terms must be checked with the provider.
7. Troubleshooting scheduled captures
| Symptom | Likely cause | Fix |
|---|---|---|
| No scheduled run appears | The workflow is not on the default branch, the schedule is configured incorrectly, or a public repository’s scheduled workflows were disabled after inactivity. | Check the default branch, YAML cron expression, and workflow status. Trigger workflow_dispatch manually to verify the workflow itself. |
| The run starts late or is missing | GitHub says schedule events can be delayed or dropped during high load. | Schedule away from minute zero. Add failure and missing-run monitoring if timing matters; do not treat cron as an exact-time guarantee. |
| Navigation times out | The site is slow, unreachable from the runner, or waiting for a lifecycle event that never settles. | Check the URL and runner logs. Use a suitable navigation event, set a justified timeout, then wait for the specific content needed. |
| The image is blank or incomplete | The page content renders asynchronously, a selector was not awaited, or the target blocks automated traffic. | Wait for a meaningful page element, inspect the response and logs, and confirm the page is accessible from the runner. |
| The screenshot differs between runs | Viewport, page data, animations, time-dependent content, or consent state changed. | Fix the viewport and capture conditions where possible. Disable or wait out changing page effects only when appropriate for the record you need. |
| Artifact is absent | The capture failed before writing the expected path, or the upload step points to a different file. | Check the capture step’s output path and ensure the upload step uses the same path. Make the script fail when navigation or capture fails. |
| Full-page image is unexpectedly huge | The document is long or includes very tall content. | Use viewport capture if it answers the monitoring question, or capture a specific element/page region through an appropriate browser workflow. |
8. Or skip the browser setup
If you need a screenshot on a schedule but do not want to install and maintain a browser runtime, call ScreenshotNeo’s screenshot API from your scheduled job. Your scheduler still determines when the request runs. The one-call API returns an image or PDF; see the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Every feature is on every plan. See ScreenshotNeo for the service and the API docs for supported parameters.
Sign up for 1,000 free screenshots a month with no card.
9. Frequently asked questions
Can I capture a page every five minutes?
GitHub Actions documents five minutes as its shortest schedule interval. Scheduled runs can still be delayed or dropped during high load, so use monitoring if missing a run has consequences.
Does the schedule run in my local timezone?
GitHub uses UTC unless you configure an IANA timezone on the schedule. Review the workflow schedule documentation for daylight-saving behavior.
Should I save screenshots in Git?
That works for a small history, but binary images accumulate in repository history. Artifacts or external storage may fit better when you need controlled retention or a larger archive.
How do I know a screenshot captured the right state?
Wait for a page-specific signal, keep viewport settings fixed, inspect the output from a manual run, and check scheduled results periodically. A successful job alone does not validate the visual contents.


