ScreenshotNeo

BlogHow-to

How to Schedule a Website Screenshot Every Week with Playwright

Use Playwright and GitHub Actions to capture a website every week, save the image as an artifact, and handle timing, readiness, and login safely.

By the ScreenshotNeo team4 October 20267 min read

Use a Playwright script to open the site and save a screenshot, then run that script from a scheduled GitHub Actions workflow and upload the image as an artifact. The example below runs every Monday at 07:30 UTC. GitHub schedules can be delayed, so treat this as a weekly capture rather than an exact-time alert.

1. Create the Playwright project

Use Node.js for this example. Playwright can also run on other CI providers; the scheduler and artifact storage are provider-specific. Create a project and install Playwright:

mkdir weekly-screenshot
cd weekly-screenshot
npm init -y
npm install --save-dev playwright
npx playwright install chromium

Commit the generated package-lock.json so CI can use npm ci. Playwright browser binaries correspond to the installed Playwright version, so install the browser in CI after installing the project dependencies. See the Playwright CI documentation.

2. Write the screenshot script

Create scripts/screenshot.mjs. It reads the target URL from an environment variable, saves a full-page PNG, and closes the browser even if navigation or capture fails.

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

const targetUrl = process.env.TARGET_URL;
if (!targetUrl) {
  throw new Error('Set TARGET_URL to the website URL to capture.');
}

const outputPath = 'screenshots/site.png';
await mkdir('screenshots', { recursive: true });

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

Run it locally with a URL to check the output before scheduling it:

TARGET_URL=https://example.com node scripts/screenshot.mjs

fullPage: true captures the full scrollable page; omit it to capture only the viewport. networkidle is a practical starting point, not a universal definition of readiness. Sites with polling, analytics, or long-lived requests may never become idle. In those cases, use a meaningful site-specific selector or readiness condition instead, for example:

await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.locator('main article').waitFor({ state: 'visible', timeout: 30_000 });
await page.screenshot({ path: outputPath, fullPage: true });

Choose a selector that indicates the content you want is actually rendered. A fixed delay can help with a known animation or delayed widget, but it is less reliable than waiting for the relevant element.

3. Schedule the weekly workflow

Create .github/workflows/weekly-screenshot.yml:

name: Weekly website screenshot

on:
  schedule:
    - cron: '30 7 * * 1' # Monday at 07:30 UTC
  workflow_dispatch:

jobs:
  screenshot:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: node scripts/screenshot.mjs
        env:
          TARGET_URL: ${{ vars.TARGET_URL }}
      - uses: actions/upload-artifact@v5
        with:
          name: weekly-website-screenshot
          path: screenshots/site.png
          retention-days: 30

Add a repository Actions variable named TARGET_URL in the repository settings. Use a GitHub secret instead if the URL itself contains sensitive information, and reference it through secrets in the workflow. Never put credentials directly in the workflow file or print them in logs.

The schedule uses POSIX cron fields: minute, hour, day of month, month, day of week. Thus 30 7 * * 1 means every Monday at 07:30. GitHub uses UTC unless a timezone is configured. Scheduled workflows run the latest commit on the default branch. Use workflow_dispatch to start a manual run when setting up or troubleshooting the workflow. For the current syntax and limits, see GitHub’s schedule event documentation.

4. Retrieve and retain screenshots

Open the repository’s Actions tab, select a workflow run, then download the weekly-website-screenshot artifact. This workflow retains each artifact for 30 days. GitHub’s default artifact retention is 90 days, but repository or organization settings can impose a different maximum. Choose a period that covers how often someone will review or download the captures; artifacts are not permanent archival storage. See GitHub’s artifact documentation.

To keep a longer visual history, download artifacts into your own storage or use a commit-based image history with care: generated image files can make repository history grow quickly. Keep the upload path limited to the screenshot you want to retain; add logs or traces only when they are useful for diagnosing failures.

5. Adjust capture behavior

Need Change
Viewport-only image Remove fullPage: true.
Different viewport Change viewport.width and viewport.height in newPage.
JPEG or another image format Use a supported screenshot type and matching output extension; Playwright supports PNG, JPEG, and WebP depending on the screenshot API options and browser.
Wait for a particular section Use locator(...).waitFor({ state: 'visible' }) with a stable selector.
Authenticated page Load Playwright storage state from a protected secret or securely provisioned file; do not commit it.
Different weekly time Update the cron expression and confirm the intended timezone.

For authenticated screenshots, Playwright storage state may contain cookies and headers that let someone impersonate the account. Protect it like a credential, restrict access, rotate it when appropriate, and exclude it from version control. Playwright explicitly discourages checking authentication state into repositories; see the authentication guide.

6. Reliability, performance, and cost

  • Scheduling: GitHub says scheduled workflow events can be delayed during high load. Do not rely on a scheduled run to happen at an exact minute. Public repository schedules are disabled after 60 days without repository activity, so check that the repository remains active if the captures matter.
  • Reproducibility: Keep the lockfile, install the browser matching the project Playwright version, and use one browser/job for a single weekly capture. Playwright’s CI guidance recommends one worker for stability and reproducibility in CI.
  • Readiness: Waiting for all network activity can be slow or never finish on sites with persistent connections. Prefer the smallest reliable readiness condition for the target page and set timeouts so the job fails with a useful error rather than hanging.
  • Runtime: A weekly job has low run frequency, but browser startup, dependency installation, page loading, and artifact upload contribute to each run. Caching npm dependencies can reduce repeated package downloads; do not cache browser binaries across incompatible Playwright versions.
  • Cost: This workflow uses GitHub-hosted Actions and artifact storage, whose availability and billing depend on the repository plan and current GitHub terms. Review your repository’s included minutes and storage limits rather than assuming the workflow is free. This example does not include a separate image-hosting service.

7. Troubleshooting

Symptom Likely cause Fix
Scheduled run does not appear Workflow file is not on the default branch, cron syntax is invalid, the repository is inactive, or GitHub delayed the event. Check the Actions tab and workflow file on the default branch; use workflow_dispatch to verify manually. Remember schedules are not exact-time guarantees.
TARGET_URL is missing The repository variable or secret is unset or referenced under the wrong name. Set the variable in repository settings and ensure the workflow reference matches it exactly.
Browser executable not found CI installed Playwright but not its browser, or the installed browser does not match the package version. Run npx playwright install --with-deps chromium after npm ci.
Navigation times out The site is slow, unreachable from the runner, or keeps connections active while waiting for network idle. Check the URL and runner access; use domcontentloaded followed by a relevant locator wait when network idle is unsuitable. Keep a finite timeout.
Image is blank or incomplete The screenshot was taken before client-rendered content appeared, a consent/login wall blocks it, or the wrong selector was used. Wait for the actual content; verify the page and selector in a local run; provide required authentication securely.
Artifact is missing Capture failed before writing the file or the upload path does not match. Check the preceding step’s logs and ensure screenshots/site.png exists and matches the artifact path.
Authentication stops working Saved cookies expired or the site invalidated the session. Refresh the state securely and update the protected CI secret or file. Never expose the state in logs or commit history.

8. Alternatives for a weekly capture

GitHub Actions is one concrete scheduler, not a requirement of Playwright. You can run the same script from another CI provider’s scheduled job or from a server’s cron service. Compare timezone support, schedule delay expectations, secret handling, artifact retention and access controls, browser installation support, and operating cost. Keep the capture script separate from the scheduler so you can move it without rewriting the browser logic.

Or skip the browser setup

If you only need a recurring screenshot, ScreenshotNeo is a website screenshot API and MCP server. A scheduled job can call its API and save the returned image; see the ScreenshotNeo API documentation.

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

The API call can replace browser installation and capture code in a scheduled task. ScreenshotNeo removes cookie or consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are not billed. Its MCP server lets AI agents take screenshots, and 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Will the workflow run at exactly 07:30?

No. The cron expression requests that time in UTC, but GitHub can delay scheduled runs during periods of high load.

Can I capture more than one website?

Yes. Run the script once per URL or adapt it to iterate over a maintained list, then upload the intended output files. Keep secrets out of that list if URLs contain credentials.

How do I keep the screenshots longer than 30 days?

Change the artifact retention within the repository or organization limits, or copy the image to storage intended for longer retention.