A halftone illustration of a Greek temple standing above a wide meadow
Open viewer ↗
Image
Built for

Preview changes without running the app

Screenmap runs it in CI, screenshots the screens your change touched, and posts them to the pull request.

What it does

Every pull request gets a comment with the screens in it

Screenmap works out which screens your change can reach, captures them on a simulator or an emulator, and posts what it found. The comment shows the part of the screen that changed. The link opens the full map.

The map is interactive

Click any screen for its route and the file it comes from.

See what changed on each screen

Screenmap shows you the old and the new version, with the parts that moved marked.

The map covers your whole app

Every screen is there, with the ones your pull request touched highlighted, so you can see where the change sits.

screenmap bot commented 4 minutes ago

Three screens changed in this pull request

Capture of /water /water new Capture of /compare /compare changed Capture of /grind /grind removed
/water new route on this branch
/compare its own source changed (compare.tsx)
/grind route gone from this branch

Before and after

1 new route · 1 route gone

How these were captured

CI

It runs on your CI, and your screenshots stay in your repo

It runs where your code already is

Everything happens in your GitHub Actions. Bundles live on a branch in your repo, and the viewer opens them in your browser.

Free

MIT licensed. You pay for CI minutes and your agent, nothing else.

iOS, Android, or both

iOS captures on a macOS runner, Android on ubuntu-latest, which bills at about a tenth of the macOS rate. In CI it is one platform per job; the two bundles fold into one map, and the viewer gets an iOS / Android switcher.

Use whichever agent you already have

Claude Code by default, with presets for Codex, Gemini and OpenCode. For anything else, give Screenmap a command and the name of its API key variable.

Claude CodeCodexGeminiOpenCode

Coming soon: skip the runner

Hosted simulators from argent cloud and Expo, so you can run the iOS side without a macOS runner of your own.

CI setup

Add it to your repo

The fast way

Let your agent do it

Read it first
Set up Screenmap in this repository.

Screenmap is a GitHub Action that maps every screen of an Expo / React Native app
and comments on each pull request with screenshots of the screens that change
touched. Source: https://github.com/aleqsio/screenmap

Work through this in order.

1. Confirm this repo is an Expo / React Native app using expo-router or a
   react-navigation route map. Find the app directory: the repo root, or
   something like apps/mobile in a monorepo. Call that PROJECT.

2. Set up the EAS side. Check the app config for extra.eas.projectId — the EAS
   lane shells out to eas fingerprint:generate and eas build, and both refuse to
   run against an unlinked project. If it is missing, run "npx eas init".
   Then look at eas.json for a build profile that produces a dev client for the
   platform you want. iOS needs developmentClient: true and ios.simulator: true,
   and Screenmap defaults to a profile named "development-simulator". Android
   needs developmentClient: true and android.buildType: "apk" — an .aab cannot be
   installed on an emulator — and defaults to "development-emulator". If yours
   are named something else, pass eas_profile with the real name. If no such
   profile exists, add one. If this repo already builds its own dev client, you
   can skip EAS entirely: pass app_path pointing at it — an .app for iOS, an .apk
   for Android — instead of expo_token.

3. Create .github/workflows/screenmap-pr.yml with exactly this:

name: screenmap · PR changes
on:
  pull_request:
    types: [opened, synchronize, reopened]
concurrency:
  group: screenmap-pr-${{ github.event.pull_request.number }}
  cancel-in-progress: true
permissions:
  contents: write        # push bundles to the screenmaps branch
  pull-requests: write   # the sticky comment
  actions: write         # start the baseline if no map exists yet
jobs:
  changes:
    runs-on: macos-26
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - uses: aleqsio/screenmap@v1
        with:
          mode: pr
          expo_token: ${{ secrets.EXPO_TOKEN }}   # EAS builds or reuses the dev client
          # app_path: build/MyApp.app   # ...or your own simulator build, with no expo_token
          # platform: ios   # or android, on runs-on: ubuntu-latest — a tenth of the macOS rate
          agent_provider: claude
          agent_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

4. Create .github/workflows/screenmap-baseline.yml with exactly this:

name: screenmap · baseline
on:
  push:
    branches: [main]
  workflow_dispatch:
concurrency:
  group: screenmap-baseline
  cancel-in-progress: false
permissions:
  contents: write
  pull-requests: write
  actions: write         # re-run PRs waiting on this map
jobs:
  baseline:
    runs-on: macos-26
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - uses: aleqsio/screenmap@v1
        with:
          mode: baseline
          expo_token: ${{ secrets.EXPO_TOKEN }}   # EAS builds or reuses the dev client
          # app_path: build/MyApp.app   # ...or your own simulator build, with no expo_token
          # platform: ios   # or android, on runs-on: ubuntu-latest — a tenth of the macOS rate
          agent_provider: claude
          agent_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          flows_pr: "true"

5. Adapt both workflows to this repo:
   - If PROJECT is not the repo root, add "project: PROJECT" under "with:".
   - If this repo is private, add 'publish: "false"' so bundles stay as run
     artifacts instead of being pushed to a branch.
   - If I asked for Android too, remember it is one platform per job: keep the
     iOS job on macOS, add a second with "platform: android" on ubuntu-latest,
     and fold the bundles together with "screenmap-ci merge --inputs
     ios=<a>.scrmap,android=<b>.scrmap". The workflow templates in the repo show
     the shape.
   - Set the agent to yourself. Work out which CLI you are running as and put
     that in agent_provider in both workflows, then set agent_api_key to the
     secret that matches it:
     - claude -> ANTHROPIC_API_KEY
     - codex -> OPENAI_API_KEY
     - gemini -> GEMINI_API_KEY
     - opencode -> AGENT_API_KEY
     If you are some other agent, use its CLI name and the env var its key
     normally lives in, and tell me which one you picked.

6. If any route takes a parameter, write .screenmap/config.json with real values
   for them. A route like /brew/[id] is deep-linked as /brew/1, and if 1 is not
   a real id the run captures the not-found screen and reports it as that
   screen. Use ids that exist in the app's fixtures, seed data or test account:

   { "params": { "id": "v60" } }

   Plain "id" covers every route with that parameter, and prefixing the route id
   ("brew/[id].id") scopes it to one route. Anything else the agent needs, like
   how to sign in, which controls to leave alone, or how slow a screen is, goes
   in .screenmap/SKILL.md.

7. Do NOT create, print, or paste any secret yourself. Instead, tell me to add
   these in Settings -> Secrets and variables -> Actions:
   - EXPO_TOKEN (required) — expo.dev -> Account settings -> Access tokens.
   - the agent key you named in step 5 (optional). Without it Screenmap still
     runs, on the routes it can reach on its own. Tell me where to get that
     provider's key.

8. Tell me that an iOS run needs a macOS runner, which bills at ten times the
   Linux rate, so one run costs about what a short iOS build costs. An Android
   run goes on ubuntu-latest, at about a tenth of that.

When you are done, summarise what you changed and what is left for me to do by
hand. Do not commit or push unless I ask.
Sets the secret and the workflows below.
  1. Before you start

    You need all four of these.

    • An Expo or React Native app using expo-router or a react-navigation route map.
    • A project set up for EAS — linked with npx eas init, with a build profile that produces a simulator build, or an apk for Android. Or a build of your own, if you would rather not use EAS at all.
    • A GitHub repo you can add secrets to.
    • Runner minutes. iOS needs a macOS runner, which bills at ten times the Linux rate, so a run costs about the same as a short iOS build. Android runs on ubuntu-latest, at about a tenth of that.
  2. Add the secrets

    In your repo: Settings → Secrets and variables → Actions.

    • EXPO_TOKEN Required

      Lets Screenmap fetch or start the build.

      Where to find it

      expo.dev → Account settings → Access tokens.

    • ANTHROPIC_API_KEY Optional

      Lets the agent explore screens with no recorded path. Without it Screenmap runs deterministically, on the routes it can reach on its own.

      Where to find it

      console.anthropic.com → API keys.

  3. Add .github/workflows/screenmap-pr.yml

    This is the one that comments on pull requests. It captures iOS, which is why the job runs on macOS. For Android, set platform: android and runs-on: ubuntu-latest.

    name: screenmap · PR changes
    on:
      pull_request:
        types: [opened, synchronize, reopened]
    concurrency:
      group: screenmap-pr-${{ github.event.pull_request.number }}
      cancel-in-progress: true
    permissions:
      contents: write        # push bundles to the screenmaps branch
      pull-requests: write   # the sticky comment
      actions: write         # start the baseline if no map exists yet
    jobs:
      changes:
        runs-on: macos-26
        timeout-minutes: 60
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0
          - uses: actions/setup-node@v4
            with:
              node-version: 22
          - uses: aleqsio/screenmap@v1
            with:
              mode: pr
              expo_token: ${{ secrets.EXPO_TOKEN }}   # EAS builds or reuses the dev client
              # app_path: build/MyApp.app   # ...or your own simulator build, with no expo_token
              # platform: ios   # or android, on runs-on: ubuntu-latest — a tenth of the macOS rate
              agent_provider: claude
              agent_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
  4. Add .github/workflows/screenmap-baseline.yml

    This one keeps the map of main up to date, so pull requests have something to compare against. When the agent works out how to reach a screen, this is also the job that opens a pull request adding that path to your repo.

    name: screenmap · baseline
    on:
      push:
        branches: [main]
      workflow_dispatch:
    concurrency:
      group: screenmap-baseline
      cancel-in-progress: false
    permissions:
      contents: write
      pull-requests: write
      actions: write         # re-run PRs waiting on this map
    jobs:
      baseline:
        runs-on: macos-26
        timeout-minutes: 120
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0
          - uses: actions/setup-node@v4
            with:
              node-version: 22
          - uses: aleqsio/screenmap@v1
            with:
              mode: baseline
              expo_token: ${{ secrets.EXPO_TOKEN }}   # EAS builds or reuses the dev client
              # app_path: build/MyApp.app   # ...or your own simulator build, with no expo_token
              # platform: ios   # or android, on runs-on: ubuntu-latest — a tenth of the macOS rate
              agent_provider: claude
              agent_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
              flows_pr: "true"
  5. Open a pull request

    Screenmap comments with the screens your change touched and a link to the full map. The first pull request also builds the map of your default branch, which it does on its own.

Where the build comes from

Screenmap fingerprints your project and looks for a finished EAS build that matches. If there is one, it downloads it, so a JavaScript-only pull request never waits on a rebuild. If there is not, it runs eas build on your usual profile. If you already produce a build of your own — an .app for iOS, an .apk for Android — pass app_path instead and Screenmap skips EAS.

Monorepos and private repos

Point it at the app with project: apps/mobile. On a private repo, set publish: "false" to keep bundles as run artifacts instead of pushing them to a branch.

Config

Screenmap runs with nothing configured. Two files in .screenmap/ fill in what only you know about the app.

Real values for route params

Deep-linking a route like /brew/[id] needs an id. Screenmap falls back to 1, which usually lands on the not-found screen. Set real values in params: a key like "brew/[id].id" applies to that one route, and a bare "id" is the fallback for every route that takes an id.

Tell your agent how to capture screens

SKILL.md is optional. Put in it whatever the agent should know before it starts tapping: how the test account signs in, which controls to leave alone (delete, sign out, purchase), how long a slow screen takes to settle.

iOS, Android, or both

platforms decides what a local run captures, and each platform has its own block for the device to use. Both in one list puts every screen in one map with a switcher. In CI the Action takes one platform per job instead, and the bundles are merged afterwards. A config written before this still works: a top-level device means iOS.

{
  "effort": "balanced",
  "scheme": "myapp",
  "platforms": ["ios", "android"],
  "ios": { "device": "iPhone 17 Pro" },
  "android": { "device": null },
  "params": { "brew/[id].id": "v60" },
  "suspects": { "depth": 2, "broadCap": 8 },
  "waits": {
    "transition": 2500,
    "network": 6000,
    "boot": 15000
  },
  "agent": {
    "enabled": true,
    "provider": "claude",
    "scan": "params",
    "maxScreens": 8
  }
}

Every key is optional. Start from the config template and the skill template.

Effort decides how much the agent double-checks

Every screen your change touched gets a screenshot at every level, and new screens and new links between them land in the comment whichever one you pick. effort decides how many of those screens the agent opens for itself, to confirm the deep link arrived where it should, find real parameters when it did not, and record a flow that makes the next run cheaper. Set it as an input on the Action or as "effort" in config.json. With no key set you get deterministic, which replays the paths you recorded without spending a token. You record those from your own terminal, once, and again whenever you add a screen.

effort The agent double-checks
deterministicno key nothing, and spends no tokens
fast the screens no recorded flow can reach
balanceddefault also param routes that already have a flow
thorough every screen the run captured
No CI

Or map your app from your own terminal

The GitHub Action drives the same Claude Code skill you can run yourself. In your own terminal it writes a .scrmap file you can open in the viewer.

Open the viewer ↗

Everything stays in the project

Captures and flows land in .screenmap/out/. Open the bundle in the viewer, or keep it as the baseline your next run diffs against.

You need a simulator or an emulator, and Claude Code

iOS needs a Mac; Android needs an emulator on adb. One local run can do both. --static renders the routes on their own if you would rather not boot a device.

claude plugin marketplace add aleqsio/screenmap
claude plugin install screenmap@screenmap
/screenmap                  # full run, packs a .scrmap
/screenmap --static         # routes only, no device
/screenmap --platform both  # iOS and Android in one map
/screenmap replay <flow>    # replay a recorded flow
/screenmap pr <number>      # a PR's changes as .diff.scrmap

Questions

Do I have to write tests first?

No. Screenmap finds your screens from the router and records the tap paths itself. You get them as a pull request.

Will it break my builds?

No. It posts a comment and nothing else.

How long does a run take?

About 12 minutes for a JavaScript-only change. Add an EAS build if your native code changed.

Does it work with bare React Native?

Yes, if you use expo-router or a react-navigation route map. The iOS lane needs a macOS runner; Android runs on ubuntu.

What about Android?

It captures on an Android emulator, and both platforms can land in one map with a switcher in the viewer. In CI it is one platform per job: iOS on macOS, Android on ubuntu, which bills at about a tenth of the rate.

Does anything change on the Linux runner?

Screen text is read with tesseract instead of Apple Vision, so fewer words come back. The run summary and the PR comment name the backend when it is not Vision.

How is this different from Storybook visual testing?

Storybook shoots the components you wrote stories for. Screenmap shoots your real app, including the screens nobody wrote a story for.

Can I try it first?

Yes. The demo map runs in your browser.