ScreenshotNeo

BlogHow-to

How to Schedule Screenshots of a Website at Several Intervals

Schedule recurring website screenshots with Playwright and GitHub Actions. Choose an interval, keep captures consistent, and understand timing limits.

By the ScreenshotNeo team4 October 20269 min read

To schedule website screenshots, combine a browser capture tool with a recurring scheduler. Playwright renders the page and saves the image; GitHub Actions, cron, or another scheduler starts the script at your chosen intervals. This guide uses Playwright with GitHub Actions for hourly, daily, or weekly captures, and covers timing limits, retention, visual consistency, and troubleshooting.

1. Choose an interval and capture method

First decide how often a capture should run and what part of the page you need:

  • Viewport screenshot: captures the visible browser area. Useful for a stable first-screen record.
  • Full-page screenshot: captures the full scrollable page, including content below the fold.
  • Element screenshot: captures a selected element, such as a chart or product panel.
  • Visual comparison: compares later captures against a baseline to find changes. Saving screenshots alone does not create comparisons or alerts.

For flexible capture logic, use a Playwright script and a scheduler. A prebuilt community action such as github-screenshot-action can reduce setup work, but check its maintenance, version, permissions, configuration, and retention behavior before adopting it.

2. Create a Playwright screenshot script

This example writes a timestamped full-page screenshot. It uses a UTC timestamp in the filename so separate runs do not overwrite one another.

import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';

const target = process.env.TARGET_URL ?? 'https://example.com';
const outputDir = process.env.OUTPUT_DIR ?? 'screenshots';
const stamp = new Date().toISOString().replaceAll(':', '-');
const outputPath = `${outputDir}/${stamp}.png`;

await mkdir(outputDir, { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
  const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
  await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 60_000 });
  await page.screenshot({ path: outputPath, fullPage: true, animations: 'disabled' });
  console.log(`Saved ${outputPath}`);
} finally {
  await browser.close();
}

Set up a small Node project and install Playwright and its browser:

npm init -y
npm install --save-dev playwright
npx playwright install chromium

Save the script as capture.mjs and run it with TARGET_URL=https://example.com node capture.mjs. Playwright supports file paths and image buffers, plus viewport, full-page, and locator screenshots. See the Playwright screenshot documentation.

Wait for the page you actually need

domcontentloaded waits until the initial HTML has been parsed, but some sites render important content afterward. If a known element indicates that the page is ready, wait for it before capturing:

await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.locator('[data-testid="main-content"]').waitFor({ state: 'visible', timeout: 30_000 });
await page.screenshot({ path: outputPath, fullPage: true });

For a fixed delay, use await page.waitForTimeout(2000), but prefer a meaningful condition when possible. Pages with polling, analytics, or streaming requests may never become idle, so a strict network-idle wait can time out. Choose a condition suitable for the site rather than relying on one delay for every page.

Capture only an element

await page.locator('#revenue-chart').screenshot({ path: outputPath });

The selector must match an element that appears on the page. If it is absent or hidden, the capture will fail or not represent the intended content.

3. Schedule the script with GitHub Actions

Add a workflow file at .github/workflows/screenshot.yml. This example runs every six hours and also supports a manual run from the Actions tab.

name: Scheduled website screenshot

on:
  schedule:
    - cron: '17 */6 * * *'
  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_DIR: screenshots
      - name: Upload screenshot
        uses: actions/upload-artifact@v4
        with:
          name: website-screenshot-${{ github.run_id }}
          path: screenshots/
          retention-days: 30

Commit the workflow to the repository’s default branch; scheduled workflows run from the latest commit on that branch. The workflow file must exist on the default branch for the schedule to run. Artifact retention is configurable, and the example keeps each run’s output for 30 days. If you need a long-term archive, choose storage and retention deliberately rather than accumulating images indefinitely.

Common cron intervals

Cadence Example cron Meaning
Every hour 17 * * * * At minute 17 of each hour
Every six hours 17 */6 * * * At minute 17 every six hours
Daily at 09:17 UTC 17 9 * * * Once a day at 09:17 UTC
Every Monday at 09:17 UTC 17 9 * * 1 Once a week on Monday

GitHub Actions uses POSIX cron syntax. Schedules default to UTC; its workflow syntax also supports an IANA timezone. The shortest supported interval is once every five minutes. The examples avoid minute zero because GitHub warns that high load can delay scheduled runs, especially at the start of an hour.

4. Choose where screenshots go

The workflow uploads screenshots as GitHub artifacts, which is convenient for short-term run history. Other choices include committing images to the repository or uploading them to object storage from the job. Consider:

  • Unique names: timestamped paths prevent later captures from overwriting earlier ones.
  • Retention: set a finite artifact lifetime or a storage lifecycle policy to control image growth.
  • Repository size: committing every image can make a repository grow quickly. Keep only images that belong in version history.
  • Access: screenshots can contain account data or private page content. Restrict workflow access and avoid capturing authenticated pages unless the storage and repository permissions are appropriate.

5. Make repeated captures comparable

A screenshot archive is useful only if you can interpret changes. Keep the browser version, operating system, viewport, device scale, locale, and capture settings stable. Rendering can vary with the host OS, browser version, hardware, power source, and headless mode.

  • Use a fixed viewport and browser version in recurring runs.
  • Wait for a meaningful page condition, and disable animations where appropriate.
  • Use a baseline comparison when the goal is to find visual regressions. Playwright Test’s toHaveScreenshot() creates a reference image on first use and compares later runs; its visual comparison guidance also describes filtering volatile page regions with stylesheets.
  • Decide how to handle timestamps, rotating content, ads, and other dynamic areas before treating a pixel difference as a meaningful change.
  • Add a separate notification or review step if a person needs to be alerted. Capturing and storing an image does not itself alert anyone.

See Playwright’s visual comparisons documentation for baseline testing details.

6. Know the scheduling limits

GitHub Actions cron is suitable for recurring automation where small delays are acceptable. It is not a real-time timer: scheduled runs can be delayed during high load, and queued runs may be dropped if load is high enough. Public repository schedules are automatically disabled after 60 days without repository activity. If exact execution times or stronger delivery guarantees matter, select a scheduler whose documented guarantees fit that requirement and monitor missed runs.

For reliability, make each capture safe to retry, use a timeout, and record enough information to diagnose a run: target URL, timestamp, exit status, and output path. If the workflow is important, alert on workflow failures and periodically confirm that expected artifacts are appearing.

7. cURL, Python, and Node.js alternatives

The browser script above is the do-it-yourself Playwright path. If your existing automation uses another language, the essential pattern is unchanged: run a capture command, name the output uniquely, and let the scheduler invoke it on a recurring schedule. The following commands call ScreenshotNeo’s screenshot API and can be placed in a cron job or other scheduler.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as output:
    output.write(r.content)

Node.js

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 import('node:fs/promises').then(({ writeFile }) =>
  writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))
);

For scheduled archival, change the fixed output name to a timestamped one, keep the API key in your scheduler’s secret store, and configure the schedule separately. See the ScreenshotNeo API documentation for request options and configuration.

8. Troubleshooting

Symptom Likely cause Fix
No scheduled run appears Workflow is not on the default branch, cron syntax is invalid, or the schedule was disabled after repository inactivity. Check the workflow on the default branch, validate the cron expression, and trigger workflow_dispatch manually. For a public repository, check whether 60 days of inactivity disabled schedules.
Run starts late or is missing GitHub Actions schedule load delays or drops queued runs. Avoid minute zero, inspect Actions history, and use a scheduler with stronger documented timing guarantees if missing a run is unacceptable.
Navigation times out The page is slow, blocked, or waiting for a load condition that never completes. Use a suitable navigation condition, increase the timeout within a reasonable limit, wait for a specific selector, and inspect the workflow logs.
Screenshot is blank or incomplete Capture happened before client-side content rendered, lazy content was not loaded, or the target page returned an error state. Wait for a visible content selector; for full-page captures, verify the page has rendered lower sections before capture.
Output is overwritten Every run writes to the same path. Include an ISO timestamp, run ID, or dated directory in the filename.
Images differ every run Dynamic content or rendering environment changed. Stabilize browser, OS, viewport, and locale; disable animations; filter expected volatile regions for visual comparisons.
Repository becomes large Every image is committed permanently. Use expiring artifacts or object storage lifecycle rules, and retain only needed history.
Locator capture fails The selector matches no visible element. Check the selector against the rendered page and wait for the locator to become visible before capture.

9. Performance, reliability, and cost

Each scheduled run starts a browser, loads the target, waits for the chosen readiness condition, and writes an image. Full-page captures and pages with heavy media can take longer and produce larger files than viewport captures. For many URLs, use bounded concurrency and timeouts rather than launching an uncontrolled number of browser contexts. Keep capture cadence proportional to how often the page can meaningfully change.

With self-hosted Playwright, account for runner time, browser installation and updates, storage, and maintenance. GitHub-hosted runner availability and billing depend on the repository and plan; check current GitHub limits for your account. A managed screenshot API can reduce browser setup and maintenance, but evaluate its supported cadence, history, alerting, price, and data handling against your requirements.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo. Schedule its one-request capture from your existing scheduler:

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o shot.webp

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers say which page verdict and billing outcome applied. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Every feature is on every plan. The API and its options are in the ScreenshotNeo docs.

Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Can I capture a site every five minutes?

GitHub Actions documents five minutes as its shortest schedule interval, but its schedule events can be delayed or dropped under load. Use it only if that timing variability is acceptable.

Will the workflow run in my local timezone?

GitHub schedules default to UTC. Its workflow syntax supports an IANA timezone; otherwise convert your desired local time to UTC and account for daylight saving changes.

Does a screenshot archive tell me when the site changed?

No. An archive preserves captures. To identify changes, compare against a baseline and add a review or notification step.

Should I use a full-page capture for every scheduled run?

Only if below-the-fold content matters. A viewport capture is smaller and quicker, while full-page capture can be more useful for page-wide monitoring.

Primary references