How to Capture Cypress Screenshots in GitHub Actions
Save Cypress screenshots in GitHub Actions, upload them as artifacts, keep only failures, and troubleshoot paths, retention, and CI failures.

Use Cypress’s built-in screenshots and upload cypress/screenshots with actions/upload-artifact. Cypress captures an explicit checkpoint when your test calls cy.screenshot(). During cypress run, it also captures a screenshot when a test fails unless screenshotOnRunFailure is disabled. The default output directory is cypress/screenshots.
The workflow below runs Cypress in Chrome and uploads screenshots when the job fails. The if: failure() condition matters: it lets the upload step run after a failed Cypress step. if-no-files-found: ignore keeps a run without screenshots from producing an artifact warning or error.
1. Add the GitHub Actions workflow
name: Cypress tests
on: [push, pull_request]
jobs:
cypress-run:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Cypress run
uses: cypress-io/github-action@v7
with:
build: npm run build
start: npm start
browser: chrome
- name: Upload Cypress screenshots
if: failure()
uses: actions/upload-artifact@v7
with:
name: cypress-screenshots
path: cypress/screenshots
if-no-files-found: ignore
Save this as .github/workflows/cypress.yml. The maintained Cypress GitHub Action README documents this pattern. Check action major versions and runner images when you edit an existing workflow because those releases change over time.

Upload screenshots on every run
Remove if: failure() when you want to publish deliberate screenshots from passing tests as well as failure screenshots:
- name: Upload Cypress screenshots
if: always()
uses: actions/upload-artifact@v7
with:
name: cypress-screenshots
path: cypress/screenshots
if-no-files-found: ignore
always() is useful when the Cypress command succeeds and you still want checkpoint images. It also allows the upload step to run after a failed test. Keep the upload after the Cypress step so the directory has been generated.
2. Capture deliberate checkpoints with cy.screenshot()
Call cy.screenshot() wherever a visual checkpoint helps diagnose a flow:
describe('checkout', () => {
it('shows the payment page', () => {
cy.visit('/checkout');
cy.get('[data-cy=continue-to-payment]').click();
cy.screenshot('checkout/payment');
cy.get('[data-cy=payment-form]').should('be.visible');
});
});
The named image is written below the screenshots folder, so this example produces a path similar to cypress/screenshots/checkout/payment.png. Cypress creates nested directories as needed. If a name already exists, Cypress appends (1), (2), and so on. Use { overwrite: true } when replacement is intentional:
cy.screenshot('login-page', { overwrite: true });
Screenshot capture is asynchronous and takes around 100 ms. A very fast UI transition can therefore change a small part of the image after the command is issued; assert the state you need before taking the screenshot.
3. Configure automatic failure screenshots
Automatic screenshots happen in headed or headless cypress run executions when a test fails. They use Cypress’s normal naming scheme with (failed) appended. To keep the default behavior explicit, set it in cypress.config.js:
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
screenshotOnRunFailure: true,
screenshotsFolder: 'cypress/screenshots',
trashAssetsBeforeRuns: true
}
});
Use screenshotOnRunFailure: false when failure images contain data you cannot retain or when another reporter owns screenshots. Cypress clears the screenshots directory before a run by default. Set trashAssetsBeforeRuns: false only when you deliberately need files from earlier runs; otherwise stale images can be mistaken for current evidence.
Changing the output folder
module.exports = defineConfig({
e2e: {
screenshotsFolder: 'artifacts/cypress/screenshots'
}
});
If you change this value, change the artifact path to match it:
- name: Upload Cypress screenshots
if: failure()
uses: actions/upload-artifact@v7
with:
name: cypress-screenshots
path: artifacts/cypress/screenshots
if-no-files-found: ignore
4. Understand paths and naming
Failure images and explicit images are placed beneath the screenshots folder. Cypress mirrors the spec structure after removing the common ancestor. The resulting path can change when the set of specs changes, so consume the artifact as a directory rather than hard-coding one absolute filename.
Keep generated files out of source control:
# .gitignore
cypress/screenshots/
cypress/videos/
They are regenerated in CI and should be retained through workflow artifacts or Cypress Cloud. The Cypress test organization guide covers the generated asset directories.
5. Upload videos separately when needed
The Cypress GitHub Action README shows a separate artifact upload for videos. Add one if your run records them:
- name: Upload Cypress videos
if: failure()
uses: actions/upload-artifact@v7
with:
name: cypress-videos
path: cypress/videos
if-no-files-found: ignore
Separate names make it clear whether a reviewer should download screenshots, videos, or both. Use if: always() for videos from successful runs as well.
6. Choose GitHub artifacts or Cypress Cloud
| Need | Best fit |
|---|---|
| Download PNGs from one workflow run | GitHub workflow artifacts |
| Failure-only retention | Artifact upload guarded by if: failure() |
| Every-run checkpoint images | Artifact upload with if: always() |
| Centralized run history, replay, and contextual failure details | Cypress Cloud |
GitHub provides actions/upload-artifact and actions/download-artifact for storing and retrieving files tied to a workflow run. Cypress Cloud is an optional hosted layer with shareable reports, Test Replay, screenshots, videos, and contextual failure details. The Cypress GitHub Actions guide describes both choices.
Decide how long artifacts should be retained and whether storage usage is worth keeping screenshots from passing runs. Failure-only uploads reduce storage and make the useful evidence easier to find.
7. Troubleshooting
No artifact appears
- The upload step was skipped: a failed job skips later steps unless the step condition permits it. Use
if: failure()for failure evidence orif: always()for every run. - The path is wrong: confirm
screenshotsFolderand make the artifactpathidentical. - No screenshot was generated: the test may have passed without calling
cy.screenshot(), or automatic failure screenshots may be disabled. Useif-no-files-found: ignorewhen an empty directory is expected.
The folder is empty even though a previous run had images
Cypress clears screenshots before each run by default. This prevents stale evidence. If preserving files between runs is intentional, set trashAssetsBeforeRuns: false, but use unique artifact names or cleanup rules so old images are not confused with current results.
The screenshot shows the wrong UI state
Wait for the state you want before capturing it, for example with cy.get('[data-cy=ready]').should('be.visible'). Remember that capture is asynchronous and can take around 100 ms.
Failure paths changed between commits
Cypress derives asset paths from the spec layout after removing the common ancestor. Adding, removing, or moving specs can change paths. Download the artifact directory and inspect its current tree instead of relying on a fixed path.
The workflow fails before Cypress starts
Check that npm run build and npm start exist in package.json, that the application listens on the expected port, and that the selected browser is available on the runner. The upload step cannot create screenshots when Cypress never launches.
8. Performance, reliability, and cost
- Capture only useful states: each explicit screenshot adds work and artifact bytes. Put checkpoints around transitions or important visual states rather than every command.
- Prefer failure-only uploads for large suites: this keeps routine runs fast and limits artifact storage.
- Use deterministic waits: assert visible selectors and stable content instead of fixed delays wherever possible.
- Keep upload after test execution: this guarantees generated files exist and allows a failure condition to collect them.
- Plan retention: GitHub artifact retention and storage settings determine how long old images remain available. Cypress Cloud is useful when you need searchable history and replay rather than isolated downloads.
- Protect sensitive data: screenshots can contain account details, tokens displayed in the UI, or customer data. Redact test fixtures and restrict artifact access accordingly.

Or skip the browser setup
For a one-off page image outside a Cypress test, ScreenshotNeo provides a website screenshot API. Its cleanup steps accept cookie or consent banners and remove more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the result in X-Page-Verdict and X-Billed headers. It also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for all options. This minimal call returns a WebP image:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo supports full-page and element captures, dark mode, device presets, custom viewports, retina scale, PDF output, custom CSS and JavaScript, clicks, waits, blocked resources, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, async webhooks, bulk capture, and a usage API. Every feature is on every plan. The free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000.
Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
FAQ
Does cy.screenshot() work in headed mode?
Yes. It is an explicit Cypress command and can be used during interactive development as well as CI runs.
Why do duplicate screenshot names get numbered?
Cypress avoids overwriting by appending (1), (2), and so on. Pass { overwrite: true } when replacing an existing name is intended.
Should screenshots be committed to Git?
No. Generated screenshot and video directories belong in .gitignore; retain them as workflow artifacts or in Cypress Cloud.
Can I upload screenshots from passing and failing tests?
Yes. Use if: always() on the artifact step and include explicit cy.screenshot() checkpoints.
What is the default screenshot directory?
cypress/screenshots, unless you change Cypress’s screenshotsFolder configuration.
When should I use Cypress Cloud?
Use it when your team needs centralized run history, shareable reports, Test Replay, or contextual failure details. Use GitHub artifacts for lightweight files tied to individual workflow runs.


