Running

Parallel and cross-browser

Both work today, on the recordings you already have. Nothing is recorded again, and nothing here spends tokens.

Run in parallel

npx saffron run --workers 4

or "workers": 4 in saffron.config.json. Each worker replays one scenario at a time in its own browser context, so cookies and storage never leak between scenarios.

What happens to a scenario that cannot simply replay (no cache yet, a stale cache, or a failure): it is set aside, and after the parallel phase it runs again with the agent, exactly as it would without --workers: a heal starts from the page where its own replay failed. The cost is one extra replay for those scenarios; a fully cached suite pays nothing.

Record and heal in parallel

npx saffron run --agent-workers 3

or "agentWorkers": 3 in saffron.config.json. By default one agent session runs at a time. With --agent-workers N, up to N run at once, each in a browser of its own: an agent attaches to a whole browser, so two sessions never see each other's tabs. The scenarios that share one recording (the rows of an outline) stay together on one worker, in order: the first records, the rest replay what it filed.

Cost Each session is paid for, and N at once spend N times as fast. The run's total is what it would have been one at a time; the bill just arrives sooner. Keep it at 1 unless recording time matters.
What runs in parallel Only the scenarios that need the agent, after the replay phase. Combine it with --workers for both.
Browsers Recording needs a Chromium-family browser, so under firefox or webkit this setting does nothing.

Where the time went

Every run reports its timing. The terminal prints the three slowest scenarios, and, with more than one worker, how busy each worker was:

workers over 41.2s: 1 92% busy · 2 88% busy · 3 90% busy · 4 61% busy · agent 23% busy
slowest: Checkout with a saved card 8.1s · Search by brand 6.4s · Sign in 3.9s

The report has the same in a timing panel: each worker's runs, its busy time and its share of the pass it worked in, then the five slowest scenarios. A worker well below the others is waiting for work: the suite has a few long scenarios, and splitting one of them up does more than another worker. Merged shards list each machine's workers by shard. In latest.json the same figures are under timing (wallMs, and per worker runs, busyMs and spanMs), and each scenario names its worker. With --no-agent, or in Firefox and WebKit, a red from the parallel phase is final: no agent could change it, so it is not run again.

How many workers Start with the number of CPU cores. Browsers are the cost, not Saffron.
Order Scenarios start in file order and finish in any order. The report keeps file order.
--no-agent Combine them in CI: --workers 4 --no-agent is the fast, zero-token gate.
Shared test data Parallel scenarios share your application. Two scenarios that edit the same record will interfere, the same as in any parallel runner. Give them their own data with {unique:name} (§16), seed what they only read with setup (§17), or keep them in one scenario.

Split a suite across CI machines

--workers is capped by one machine's cores. To go further, give the suite to several machines and put the results back together afterwards:

npx saffron run --no-agent --shard 1/4     # on the first machine
npx saffron run --no-agent --shard 2/4     # on the second, and so on
npx saffron report --merge                 # once, after all four

Each shard runs a fixed part of the suite. The split is by scenario, not by file: the scenarios are sorted and every fourth one goes to the same shard, so one large feature file is spread over all four machines. The rows of a Scenario Outline share one recording, so they stay together on one shard, and a large outline is placed first so the load stays even. Every machine works out the same split from the same checkout, so each scenario runs exactly once. --shard combines with --workers and --filter.

To keep each feature file on one machine instead, add --shard-by file (or "shardBy": "file" in the config):

npx saffron run --no-agent --shard 2/4 --shard-by file

Use it when the scenarios of a file share something expensive or fragile: a Background that seeds data, an account the application only lets log in from one place at a time, a file written to run in order. Whole files go to the machine with the fewest scenarios so far, largest file first, so the machines still get a similar load, but one very large file can make its machine the slowest. Every shard must use the same mode; the merge refuses a mix.

A shard does not write latest.json or add to history.jsonl: a quarter of a run is not a run, and the trends would read it as one. It writes .saffron/reports/shard-2-of-4.json (and an HTML view of its own part) instead. saffron report --merge reads every shard-*-of-*.json under .saffron/, or under the files and folders you name (searched recursively, wherever a CI artifact download put them), and writes latest.json, latest.html and one history line, as if the suite had run on one machine: the same totals, the same scenario order, the same trends.

The merge refuses, with exit code 2, anything that would not add up to the whole run:

A shard is missing Named: missing shard 2/4. A merge without it would under-count the run.
A shard was interrupted Named, with how many of its scenarios did not run. A shard stopped with Ctrl-C writes its report of what finished, marked interrupted; run that shard again. saffron import still takes what such a shard holds, and the imported report is marked interrupted.
The same shard twice Both files are named. When a re-run wrote a shard again, the latest attempt's report is merged and the earlier one is named and left out.
Shards ran different suites Every shard must run the same paths and --filter.
Shards ran against different base URLs A staging shard and a production shard are two runs, not one. Merge one environment at a time.
A shard's history is malformed Each history line in a shard file is checked field by field (numbers are numbers, the status is green, yellow or red) before it is merged, so a hand-edited or tampered CI artifact cannot put markup into the dashboard.
Shards come from different CI runs Saffron records the run from GITHUB_RUN_ID, CI_PIPELINE_ID (GitLab), BUILD_BUILDID (Azure) or CIRCLE_WORKFLOW_ID, or SAFFRON_RUN_ID when you set it.

Merging the same shards again rewrites the report but does not count the run twice. Re-running one shard, say after a flaky machine, and merging again replaces that run's history line with the new result, so the trends show what the run finally was. In CI the run is identified by the CI run id, the suite and the base URL, so this works whichever shard was re-run. Outside CI, set SAFFRON_RUN_ID to something stable for the same effect. A shard with nothing to run (more machines than scenarios) still writes its report, so the merge knows the set is complete.

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npx playwright install chromium --with-deps
      - run: npx saffron run --no-agent --workers 2 --shard ${{ matrix.shard }}/4
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: saffron-shard-${{ matrix.shard }}
          path: |
            .saffron/reports/shard-*.json
            .saffron/artifacts/
  report:
    runs-on: ubuntu-latest
    needs: test
    if: always()
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - uses: actions/download-artifact@v4
        with:
          pattern: saffron-shard-*
          path: .saffron
          merge-multiple: true
      - run: npx saffron report --merge
      - uses: actions/upload-artifact@v4
        with:
          name: saffron-report
          path: .saffron/reports/latest.*
The build verdict Each shard exits 1 on red, like any run, so the test jobs are the gate. The merge exits 0 when it wrote the report.
History Commit history.jsonl from the merge job, not from the shards, if you keep trends in git.
Screenshots The merged HTML embeds the failure screenshots it finds under .saffron/artifacts/. The example uploads them with each shard report and downloads everything back into .saffron/; scenarios never share a folder, so the shards cannot overwrite each other's. Without them the report is complete apart from the pictures.
Setup and teardown They run on every shard, and receive SAFFRON_SHARD (2/4), so a seed script can keep the machines' data apart (§17).
Recording under shards Works like any run: a shard with the agent enabled records or heals its own scenarios and files proposals on that machine. In CI, --no-agent is the usual choice.

Run on another browser

npx saffron run --browser chrome
npx saffron run --no-agent --browser firefox

or "browser": "firefox" in saffron.config.json. Five browsers are supported:

--browser What it is Replay Record and heal Where it comes from
chromium (default) The open-source Chromium build Yes Yes Installed with Saffron
chrome Google Chrome Yes Yes The copy installed on the machine
msedge Microsoft Edge Yes Yes The copy installed on the machine
firefox Firefox (Gecko engine) Yes No npx playwright install firefox
webkit WebKit, the engine behind Safari Yes No npx playwright install webkit

Recordings are browser-neutral: targets are roles, names and labels, not engine-specific selectors. Record once, replay on every engine. Recording and healing need a Chromium-family browser (chromium, chrome or msedge), because the agent attaches to the browser over CDP. Under Firefox or WebKit a scenario that needs the agent fails with that instruction instead of a confusing error, so a red there means "record this on Chromium first" or a genuine cross-browser difference in your app.

Google Chrome and Microsoft Edge

npx saffron run --browser chrome
npx saffron run --browser msedge

chromium is the open-source build Saffron ships with. chrome and msedge are the branded browsers your users actually have, with their own codecs, policies and release schedule. Use them when "it works in Chromium" is not enough evidence for your team. Everything works on them, recording and healing included, so you can also make one of them your everyday browser:

{ "browser": "chrome" }
Where the browser comes from The copy already installed on the machine. Saffron does not download it. On a CI image without it, npx playwright install chrome or npx playwright install msedge adds it.
When it is missing The run stops before the first scenario, exit code 2, and names the browser that is not installed.
Recordings The same cache files. A scenario recorded on chromium replays on chrome and msedge, and the other way round.

Several browsers in one run

npx saffron run --browser chromium,firefox,webkit --workers 4

or "browser": ["chromium", "firefox", "webkit"] in the config. The suite runs in each browser, one after another, into one report: every scenario has a row per browser, marked with its browser, in the terminal, the report, the history and the dashboard.

Order Chromium-family browsers go first, whatever the order you gave: they record and heal, so Firefox and WebKit then replay what they filed instead of failing for want of a recording.
Before anything runs Every browser starts first. One that is missing stops the run, exit 2, before any recording is touched.
Evidence Each browser keeps its own screenshots and traces, under .saffron/artifacts/<feature>/<scenario>/<browser>/. saffron trace lists them as feature:scenario@browser.
Dashboard A scenario stands at its worst browser, among the browsers the last run used: move from one browser to another and the one you left no longer counts. A scenario red in Firefox and green in Chromium is a cross-browser bug, not a flaky test: the dashboard compares each browser with itself.
Running one browser again Every result names its browser, matrix or not. saffron run --browser chromium (or one scenario of it, features/checkout.saffron:Pay) replaces Chromium's rows and leaves Firefox's and WebKit's as they were, in the report and on the dashboard. A result from before Saffron recorded browsers counts as any browser's, and the next one replaces it.
For tools Rows in latest.json, history.jsonl and the lastRun.attention list of saffron status --json (and the MCP project_status tool) carry browser, and so do a CI run's rows in saffron report --run <id> --json. Both list the run's browsers when its rows span several, and the browser labels show only then. In VS Code and JetBrains, Last Run and CI Runs label such rows with their browser, and Open Replay opens that browser's execution alone.
With --shard Refused: give each shard job one --browser. A shard records its browser, and saffron report --merge refuses shards of different browsers: merge each browser's shards on their own, and each gets its own history line. A CI matrix (below) runs the browsers side by side, which a single machine cannot.

Locally, one command and one report is what this is for. In CI, a matrix of jobs is faster.

Devices and viewports

npx saffron run --device "iPhone 15"
npx saffron run --viewport 1280x720
npx saffron run --browser chromium,webkit --device "Pixel 7"

or "device" and "viewport" in the config. A device is one of Playwright's profiles ("iPhone 15", "Pixel 7", "iPad Pro 11", "Desktop Chrome"...): it sets the viewport, the user agent, touch and the pixel ratio for every scenario, the agent's recordings and heals included. A viewport sets the size alone, and wins over the device's when both are given. A name Playwright does not know exits 2 with the closest names.

Recordings are not tied to a profile. A scenario recorded on the desktop replays under a phone profile, and passes where the page works the same, or fails at the step where it does not (a menu behind a hamburger button, say): that red is the finding. Record under the profile instead when the phone flow differs on purpose. Playwright does not emulate mobile devices in Firefox, so a mobile profile with --browser firefox stops the run, exit 2; give Firefox a --viewport instead.

A cross-browser matrix in CI

strategy:
  matrix:
    browser: [chromium, chrome, msedge, firefox, webkit]
steps:
  - run: npm ci
  - run: npx playwright install ${{ matrix.browser }} --with-deps
  - run: npx saffron run --no-agent --strict --workers 4 --browser ${{ matrix.browser }}

Five jobs, zero tokens, no credentials: replay needs no AI provider.