ScreenshotNeo

BlogHow-to

How to Schedule Screenshots of a Password-Protected Web App with Playwright

Schedule Playwright screenshots in CI, reuse authentication state safely, wait for the app to load, and save each capture as an artifact.

By the ScreenshotNeo team4 October 20269 min read

Direct answer: Run a Playwright script or test from a scheduled CI workflow. Authenticate to the app, confirm that the protected page is ready, capture it with page.screenshot(), and upload the output as a CI artifact or store it somewhere with the access controls and retention you need. Keep authentication state private: Playwright state files can contain credentials that let someone impersonate the account.

1. Choose where the scheduled job runs

A hosted CI runner is a practical starting point for recurring captures: the scheduler starts a job, and that job installs dependencies and browser binaries before running Playwright. A self-hosted runner or always-on machine can suit workloads that need a persistent environment, but adds runner maintenance.

Compare execution cost or limits, secret handling, schedule and timezone behavior, artifact access and retention, and maintenance. These vary by provider; check its current documentation before relying on a particular schedule syntax or retention period. Playwright’s [CI guide](https://playwright.dev/docs/ci) covers CI execution patterns, including GitHub Actions and self-hosted CI.

For one capture flow, a standalone Node.js script is enough. Use Playwright Test if you want test fixtures, setup projects, reports, retries, or visual screenshot assertions. This is a design choice based on the features each API provides.

2. Create a standalone capture script

The following example expects a Playwright storage-state file created by a separate authenticated setup step. It saves a viewport screenshot, checks that an application-specific element is visible, and fails if the expected page is not reached.

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

const targetUrl = process.env.TARGET_URL;
const statePath = process.env.PLAYWRIGHT_STATE_PATH ?? 'playwright/.auth/state.json';
const readySelector = process.env.READY_SELECTOR ?? '[data-testid="dashboard"]';
const outputPath = process.env.OUTPUT_PATH ?? 'artifacts/dashboard.png';

if (!targetUrl) throw new Error('Set TARGET_URL to the protected page URL');

await mkdir('artifacts', { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
  const context = await browser.newContext({ storageState: statePath });
  const page = await context.newPage();
  await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 45_000 });

  // This should be an app-specific element that appears only after successful login.
  await page.locator(readySelector).waitFor({ state: 'visible', timeout: 30_000 });
  await page.screenshot({ path: outputPath, type: 'png' });
  console.log(`Saved screenshot to ${outputPath}`);
  await context.close();
} finally {
  await browser.close();
}

Install the package and browser binary in the project, then run the script:

npm install --save-dev playwright
npx playwright install chromium
TARGET_URL='https://app.example.com/dashboard' node capture.mjs

Replace the example URL and readiness selector with values from your app. Do not put credentials directly in the script or commit the state file.

3. Authenticate and save state

Prefer the application’s supported authentication API when one is available; it can avoid brittle UI steps. Otherwise, automate the login page and save browser storage state after the app confirms authentication. Playwright supports cookies, local storage, IndexedDB, and passkey-based authentication in storage state. Session storage is not persisted directly by its storage-state API and needs a separate save-and-restore approach.

// setup-auth.mjs
import { chromium } from 'playwright';
import { mkdir } from 'node:fs/promises';

const loginUrl = process.env.LOGIN_URL;
const username = process.env.APP_USERNAME;
const password = process.env.APP_PASSWORD;
if (!loginUrl || !username || !password) {
  throw new Error('Set LOGIN_URL, APP_USERNAME, and APP_PASSWORD');
}

await mkdir('playwright/.auth', { recursive: true });
const browser = await chromium.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.goto(loginUrl, { waitUntil: 'domcontentloaded', timeout: 45_000 });
  await page.getByLabel('Email').fill(username);
  await page.getByLabel('Password').fill(password);
  await page.getByRole('button', { name: 'Sign in' }).click();

  // Use a stable authenticated signal from your own app.
  await page.getByTestId('dashboard').waitFor({ state: 'visible', timeout: 30_000 });
  await page.context().storageState({ path: 'playwright/.auth/state.json' });
} finally {
  await browser.close();
}

Adapt the locators to the actual login form. Playwright’s [authentication guide](https://playwright.dev/docs/auth) recommends keeping saved authentication files in a git-ignored directory and warns that they may be sufficient to impersonate the account.

Add the state directory to .gitignore, for example:

playwright/.auth/

Inject the login secrets through the CI provider’s secret facility. Avoid printing state contents, cookies, tokens, or passwords in logs. Restrict who can access job output and uploaded artifacts. Recreate the state when the session expires. If concurrent jobs mutate shared server-side data, use separate accounts per worker; a shared read-only account is more appropriate when captures do not change state.

4. Schedule and run it in CI

The provider-specific schedule configuration is not universal. Set the schedule using the provider’s current documentation, including its timezone rules and branch requirements. The workflow below shows the execution sequence for GitHub Actions; add a schedule trigger using GitHub’s current workflow syntax and set the intended branch and time deliberately.

# .github/workflows/capture.yml
name: Scheduled page capture

on:
  workflow_dispatch:
  # Add the provider's documented schedule trigger here.

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: Write auth state from CI secret
        run: |
          mkdir -p playwright/.auth
          printf '%s' "$PLAYWRIGHT_STATE_JSON" > playwright/.auth/state.json
        env:
          PLAYWRIGHT_STATE_JSON: ${{ secrets.PLAYWRIGHT_STATE_JSON }}
      - name: Capture page
        run: node capture.mjs
        env:
          TARGET_URL: ${{ vars.TARGET_URL }}
          PLAYWRIGHT_STATE_PATH: playwright/.auth/state.json
          READY_SELECTOR: '[data-testid="dashboard"]'
      - uses: actions/upload-artifact@v4
        with:
          name: scheduled-screenshot
          path: artifacts/
          if-no-files-found: error

In this example, the storage-state JSON is supplied as a secret. You can instead run an authentication setup script using separately stored login secrets, then capture in the same job. Keep generated state out of artifacts unless there is a specific, access-controlled reason to retain it. The screenshot artifact itself may expose private account data, so control access and configure retention in the CI provider.

Playwright’s [CI documentation](https://playwright.dev/docs/ci) recommends one worker in CI for stability and reproducibility; parallel workers or sharding may help on capable infrastructure when the job needs more throughput. A single browser capture often does not need parallelism.

5. Pick the capture shape and readiness condition

  • Viewport: the default screenshot captures the visible viewport.
  • Full page: pass fullPage: true to capture the whole scrollable page. This can produce a very tall image and is not equivalent to a single-screen view.
  • Image type: Playwright supports PNG, JPEG, and WebP. Use an output extension consistent with the chosen type.
  • Scale: screenshot options include CSS-pixel and device-pixel scaling. CSS-pixel output can make dimensions more consistent across device pixel ratios; device-pixel output preserves higher-density dimensions.
  • Dynamic content: disable animations or mask changing elements when repeat captures need to be visually comparable. A clock, rotating banner, or live data can otherwise make each image differ.
await page.screenshot({
  path: 'artifacts/full-page.webp',
  type: 'webp',
  fullPage: true,
  scale: 'css',
  animations: 'disabled',
  mask: [page.locator('[data-testid="live-value"]')],
});

Only use masking where hiding that content makes sense for your purpose. For capture readiness, prefer a page-specific visible element or an expected final URL over a fixed delay. A fixed delay can be useful for a known animation or delayed widget, but it can be both slower than needed and too short when the page is slow. Network-idle conditions are not always a reliable signal for apps that keep requests open; choose a condition tied to the state you need.

6. Keep captures useful over time

For a scheduled visual history, make the page state as repeatable as practical: use a read-only account, a stable viewport, a meaningful ready selector, and a consistent image format and scale. Mask or disable volatile content if the capture is meant for comparison. If the goal is visual regression, Playwright Test’s toHaveScreenshot() assertion compares against a baseline; keep that baseline workflow distinct from an archive of scheduled production appearances.

CI artifacts are convenient for inspection, but their access and retention are provider settings. If images must persist longer or be consumed by another system, copy them to a storage destination you control and set its permissions and retention explicitly. Avoid public storage for screenshots containing protected data.

7. Troubleshooting

Symptom Likely cause Fix
Capture shows the login page State is missing, expired, or belongs to another environment; login did not finish. Check that the CI secret contains valid state for the target host. Wait for an authenticated UI element or final URL before saving state and before capture.
State file cannot be read The path is wrong, the file was not created, or the secret is not valid JSON. Confirm the setup step and PLAYWRIGHT_STATE_PATH agree. Validate the JSON without printing its sensitive contents.
Timeout waiting for the ready selector The selector changed, the app is still loading, or authentication failed. Inspect a protected run’s page URL and safe console output, then use a stable selector that exists only after successful login. Increase the timeout only if the page legitimately needs longer.
Browser executable is missing Playwright package is installed but the browser binary or OS dependencies are not. Run npx playwright install --with-deps chromium in the CI job, or use the documented browser setup for the chosen runner.
Screenshot is blank or incomplete Capture started before app content rendered, content is lazy-loaded, or the selected capture area is wrong. Wait for the content-specific ready signal. For a full-page capture, check whether the app loads content only after scrolling and use an app-specific approach to reveal it.
Job succeeds but no artifact appears Output path differs from upload path, or the script wrote no file. Use the same artifacts directory in the script and upload step; fail when no files are found.
Repeated screenshots differ Live data, animation, time, or random content changed between runs. Use a stable account and viewport; disable animations or mask only the volatile region if appropriate.
Scheduled run does not happen at the expected time Provider schedule timezone, branch, or workflow rules differ from assumptions. Check the provider’s current schedule documentation, confirm the workflow is on the required branch, and verify its timezone behavior.

8. Performance, reliability, and cost

A capture run must start a runner, install or reuse dependencies and browsers, authenticate, load the page, and upload its output. Cache dependencies where the provider supports it, keep the browser set to the one you need, and avoid unnecessary full-page captures if a viewport image answers the question. Set explicit navigation and readiness timeouts so a stalled page fails clearly instead of occupying a runner indefinitely.

Reliability depends on more than Playwright: session lifetime, application availability, runner availability, schedule semantics, and artifact configuration all matter. Decide how a failed run should be noticed, and test that the job fails when login or readiness checks fail. Provider usage limits and prices vary; this research does not establish specific costs. Track run frequency and duration in the selected CI provider before increasing capture cadence.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Its one-call API can return a screenshot or PDF without maintaining a Playwright runner for the capture:

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

See the ScreenshotNeo API documentation for the request options. For this password-protected-app workflow, an authenticated page still requires an appropriate authentication method; do not assume a plain URL capture can access a private session.

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, no card required.

FAQ

Can the screenshot job use my normal user account?

Use an account with only the access needed for capture, preferably read-only. Treat its saved state as a credential and restrict access to it and the resulting screenshots.

Should I save a new storage state on every run?

Only if your authentication flow or session lifetime requires it. Reusing valid state is simpler; expired state needs refreshing through a supported login flow.

Can I capture a page that requires session storage?

Playwright’s storage-state API does not persist session storage directly. Implement a separate save-and-restore step for that application state, and protect it as a credential.

Is an artifact a permanent screenshot archive?

Not by default. Retention and access depend on the CI provider. Configure them explicitly or copy the image to controlled storage when longer retention is required.

Sources: Playwright CI, Playwright authentication, Page screenshot API, and Playwright visual comparisons.