TL;DR

This article covers how to wire Playwright's planner, generator and healer agents into a real CI pipeline without letting them decide whether a PR merges. It walks through where each agent runs, the deterministic merge gate, repair agents on CI failures, time and token budgets, debugging agent runs with traces, and judging agent-written tests against human-written ones.

Let agents author and repair tests off the merge path, and let only reviewed, committed specs decide whether a PR merges. Agents propose, and a plain Playwright run with no model in it decides.

  1. Review the planner's Markdown plan before the generator writes any code, and land generated specs as a pull request reviewed like any other code.
  2. Run committed specs in the merge gate with --run-agents=none, and add a second job that repeats new or changed agent-written specs before they merge.
  3. Trigger a repair agent from a real CI failure, classify the failure before anyone changes code, and send a repair out as a draft PR that a person approves.
  4. Set globalTimeout on every run an agent starts, and back it with a timeout on the CI job.
  5. Judge agent-written specs on flake rate, retry count and duration, with retries on in CI and failOnFlakyTests set.

Coding agents made feature work faster, and the E2E suite didn't keep pace. Every new screen still needs a spec, and every UI change still breaks a few old ones. So the test work is moving to agents too: one explores the app, one writes the test, one repairs it when CI goes red.

That is where the trouble starts. Playwright MCP operates on the page's accessibility tree, not pixels: each tool it runs returns a structured snapshot of the page's elements, their roles and text content. A merge gate has to give the same verdict for the same commit every time, or a red build stops meaning anything.

What makes testing agentic is who decides the next step. In AI-assisted testing, a human decides and a model suggests code. In AI-generated testing, a model writes the whole test in one shot. In agentic testing, an agent loops: it reads the app's state, acts, and decides again.

Playwright comes with three Test Agents out of the box: a planner, a generator and a healer. The generator transforms the Markdown plan into Playwright Test files, and the healer automatically repairs failing tests. The Playwright docs say the agents can run on their own, one after another, or chained in an agentic loop, and that running them in sequence produces test coverage for your product. Whether to use them is the easy question. Where each one runs in CI is the hard one.

Get the placement right, and agents author and repair tests off the merge path while only reviewed, committed specs decide whether a PR merges.

Where agents fit in CI

Every agent belongs somewhere other than the merge gate. Each kind gets a place in the pipeline where a person reviews its output before that output can touch a merge:

Agent workWhere it runsWhat it hands over
Exploration and planningA feature branchA Markdown plan to review
GenerationA pull requestSpec files, reviewed like any code
Repair and triageAfter a real CI failureA classification, plus a draft PR when the test is stale
NoneThe merge gateNothing: committed specs run with no model

Agents propose. A plain Playwright run with no model in it decides.

Live checks are not verdicts

The generator checks its work as it goes. Playwright's docs say it turns the Markdown plan into executable Playwright tests and verifies selectors and assertions live while it performs the scenarios. The same docs say generated tests may still include initial errors, which the healer can fix automatically. So the live check is part of writing the test. It is not the verdict.

Anti-pattern

The agent grades itself

An agent writes or heals a test, runs it until it goes green, and the team takes that green run as proof the test works. The agent chose the change and judged it. From inside that loop, a test fixed to match the app and a test bent to match a bug look the same.

Fix

Treat everything an agent hands over as a proposal. The verdict comes from a separate Playwright run of the committed spec, in a CI job no agent touches.

Goals and fixed paths

"A user can upgrade to Pro" is a goal, and an agent can reach it by whatever route the page offers. "Open billing, pick the annual plan, confirm, see the Pro badge" is a fixed path, and a spec that walks it fails the moment one step changes. Keep critical paths as deterministic specs. Agents belong in the layer above them, exploring what the specs don't cover and debugging what they catch.

Autonomy per step

Core regression that runs on every commit stays fully specified, with every locator and assertion written down. Give an agent a bounded step only where user state or a feature flag changes the path, and pin the outcome with an exact assertion right after it.

Adoption order

Start with repair and triage, move to generation on new surfaces, and add exploration last. A repair starts from a real CI failure with a trace behind it. Generation starts from a plan someone reviewed. Exploration starts from a request and a seed test, so it has the least evidence behind it and earns trust last.

How agents see the page

An agent never sees the page the way your users do. Playwright MCP works on the page's accessibility tree, not pixels: each tool returns a structured snapshot of the page's elements with their roles and text content. The agent's picture of the page is that tree, as of the last snapshot.

Elements in a snapshot carry refs such as [ref=e2]. Refs stay stable within one snapshot, but the page changing invalidates them, so an agent should re-snapshot after every navigation. An agent that skips that step acts on refs from a page that no longer exists.

Refs in that form also show up inside a test. page.ariaSnapshot() reports refs like [ref=e2] when called in "ai" mode, and those refs resolve against the latest snapshot taken in the element's frame. When an agent-written test goes wrong at a step, capture that snapshot at the step and compare it with what the agent acted on:

import { test, expect } from '@playwright/test';

test('applies a discount code at checkout', async ({ page }, testInfo) => {
  // assumes use: { baseURL } is set in playwright.config.ts
  await page.goto('/cart');
  await page.getByRole('button', { name: 'Apply code' }).click();

  // The agent-written test diverged here: record what an agent would see
  const snapshot = await page.ariaSnapshot({ mode: 'ai' });
  await testInfo.attach('aria-snapshot-ai', { body: snapshot, contentType: 'text/plain' });

  await page.getByRole('textbox', { name: 'Discount code' }).fill('SPRING10');
  await expect(page.getByTestId('cart-total')).toHaveText('$45.00');
});

Read the attachment next to the agent's transcript. If the agent targeted a role or name that isn't in the snapshot at that step, it worked from an older page.

MCP or CLI

Use MCP for interactive authoring, where you watch the agent and steer it. Use the Playwright CLI where tokens matter, because its concise output avoids loading large tool schemas into the model's context. That makes the CLI the better fit for anything an agent runs unattended.

Anti-pattern

Every snapshot element is reachable

One reporter saw an agent read a Playwright MCP snapshot that listed footer links below the fold and generate test cases for them, although a user would first have to scroll (#39955). The issue was closed as not planned. A maintainer replied that this doesn't mean the request is wrong and asked anyone who still needs it to open a new issue against the latest Playwright version.

Fix

Read a snapshot as a list of what exists on the page, not of what a user can reach. Review each generated step against the plan's user flow, and reject a step that jumps to an element the flow hasn't reached yet. Then let a deterministic run of the generated test show whether the steps work. A snapshot never proves that.

Authoring off the merge path

Authoring is where agents save the most time, and where unreviewed output does the most damage. Keep all of it on a branch or in a PR.

Agent definitions

Add the agent definitions with init-agents and commit them like any other config. The docs say to regenerate them whenever Playwright is updated, so they pick up new tools and instructions. Make that part of every upgrade PR:

# Run once on setup, then again in every PR that upgrades Playwright
npm install --save-dev @playwright/test@latest
npx playwright init-agents

The seed test

The planner runs the seed test to do all the initialization your tests need: global setup, project dependencies, fixtures and hooks. A seed that's a bare page.goto hands the planner a logged-out page with none of your data, and the plan it writes describes an app your suite never sees. Build the seed on the fixtures the suite already uses:

// tests/fixtures.ts
import { test as base, expect, type Page } from '@playwright/test';

export const test = base.extend<{ signedInPage: Page }>({
  signedInPage: async ({ page }, use) => {
    await page.goto('/login'); // needs use.baseURL in playwright.config.ts
    // Credentials come from the environment, never from a plan or prompt
    await page.getByLabel('Email').fill(process.env.E2E_USER_EMAIL!);
    await page.getByLabel('Password').fill(process.env.E2E_USER_PASSWORD!);
    await page.getByRole('button', { name: 'Sign in' }).click();
    await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
    await use(page);
  },
});

export { expect };
// tests/seed.spec.ts
import { test, expect } from './fixtures';

test('seed', async ({ signedInPage }) => {
  await expect(signedInPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

The fixture reads credentials from environment variables instead of hard-coding them in the spec or the seed.

The plan as contract

The planner's Markdown plan is the cheapest place to catch a wrong test, so review it before the generator writes any code. Make it state what a reviewer needs to check: the intent of each scenario, the business rules behind it ("the cheapest fare is preselected", "a trial user can't export"), and the negative and boundary cases, such as an expired card or a cart at its item limit. Plain language works here because the people who know the rules can read it. The generated code gets checked against the plan, so a vague plan gives you code nobody can check.

Generated specs land as PRs

Generated tests can start with errors, so nothing the generator writes goes straight to main. Its specs arrive as a pull request and get reviewed like any other code. Reviewers who want a list to work from can use our pre-merge checklist for AI-generated Playwright tests. Exploration follows the same rule: an agent bug bash on a branch ends in a plan or an issue, not in a commit to main.

Anti-pattern

Every tool on, secrets in the prompt

A team sets up one shared MCP config for everyone, enables the tool that runs arbitrary JavaScript in the Playwright server process, and pastes a real login into the prompt so the agent gets past auth. Playwright's docs call that tool RCE-equivalent and say to enable it only for trusted MCP clients. Any client of that server can now run code on the machine, and the credentials sit in the model's context.

Fix

Leave the arbitrary-JavaScript tool off in any config an untrusted client can reach. Keep credentials in environment variables that your fixtures read, as the seed above does, and out of every plan, prompt and committed file.

The deterministic merge gate

The merge gate runs committed spec files and nothing else. Agents propose code in branches and PRs, and they never run inside the gating job.

Playwright controls this with the --run-agents option, which runs agents to generate the code for page.perform. The option accepts missing, all or none, and none is the default.

Anti-pattern

Agents in the gating run

A team wants the gate to keep up with UI changes on its own, so the PR workflow runs the suite with agents generating the code for every page.perform step:

# Anti-pattern: agents generate page.perform code on every gating run,
# so one commit can take different paths and get different verdicts
name: PR gate
on: pull_request
jobs:
  e2e:
    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
      - run: npx playwright test --run-agents=all
        env:
          AGENT_API_KEY: ${{ secrets.AGENT_API_KEY }}  # placeholder: the credential your agent's model provider needs

An agent's path can differ from one run to the next, so the same commit can pass once and fail the next time, and nobody can say what either run checked.

Fix

Run committed specs with --run-agents=none, and add a second job that repeats new or changed agent-written specs before they merge:

name: PR gate
on: pull_request
jobs:
  gate:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx playwright install --with-deps
      # Committed specs only: no model runs in the gate
      - run: npx playwright test --run-agents=none
  repeat-agent-specs:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx playwright install --with-deps
      - name: Repeat new or changed agent-written specs
        run: |
          CHANGED=$(git diff --name-only --diff-filter=AM "origin/${{ github.base_ref }}...HEAD" -- 'tests/agent/*.spec.ts')
          if [ -z "$CHANGED" ]; then
            echo "No agent-written specs changed"
            exit 0
          fi
          npx playwright test $CHANGED --run-agents=none --repeat-each=10

The --repeat-each option runs each test N times, with a default of 1. With retries on in CI and failOnFlakyTests set (the config is under judging agent-written tests below), Playwright exits with an error if any tests are marked as flaky.

Generated code as frozen replay

By default, agents don't run to generate the code for page.perform: the --run-agents option defaults to none. Committed generated code changes only when someone regenerates it, and the regenerated diff goes through review again like any other change.

Assertions as postconditions

Follow every agent-generated step with exact expect assertions. They are postconditions the agent can't edit at runtime: whatever route the generated code took, the test still has to end on the state you wrote down. A step that reached the wrong state fails right there, on the assertion.

Repair agents on CI failures

A repair agent is only as good as the evidence it starts from. Trigger it from a real CI failure, and give it more than the log. A CI log doesn't show what was on the page, which request returned the wrong data, or whether the test failed the same way last week. An agent working from the log alone can't see any of that, so it guesses, which is why a coding agent needs the failing run, not just the log.

Anti-pattern

Auto-merging the green diff

The guess often looks like a fix. The agent adds a wait, loosens the assertion or retries until the test passes, and the bug stays in the code. Auto-merge that diff and a real regression becomes a test adjusted to match it:

import { test, expect } from '@playwright/test';

// Anti-pattern: the "repair" waits out the symptom and accepts any total,
// so the test now passes against the pricing bug it caught
test('order total includes shipping', async ({ page }) => {
  // assumes use: { baseURL } is set in playwright.config.ts
  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Place order' }).click();
  await page.waitForTimeout(5000); // added by the agent
  // was: await expect(page.getByTestId('order-total')).toHaveText('$54.00');
  await expect(page.getByTestId('order-total')).toContainText('$');
});

Fix

Classify the failure before anyone changes code: regression, stale test, environment or flake. A regression goes to the people who own the feature, not to the agent. A repair goes out as a draft PR that a person approves. Run the agent in its own job, after the gate has already failed:

name: PR gate
on: pull_request
jobs:
  gate:
    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
      - run: npx playwright test --run-agents=none
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: test-results
          path: test-results/
  repair:
    needs: gate
    if: failure()
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
      - run: npx playwright install --with-deps
      - uses: actions/download-artifact@v4
        with:
          name: test-results
          path: test-results/
      - name: Ask the coding agent for a classification and a draft PR
        run: ./scripts/repair-agent.sh
        env:
          AGENT_RUN: '1'
          AGENT_API_KEY: ${{ secrets.AGENT_API_KEY }}
          REPAIR_PROMPT: >-
            A Playwright test failed in CI. Its traces are in test-results/.
            Explore them with npx playwright trace, and attach to the failing
            test with xvfb-run npx playwright test --debug=cli. First classify the failure
            as regression, stale test, environment or flake. Change code only for
            a stale test, never by adding a wait or loosening an assertion, and
            open the change as a draft pull request. For anything else, report
            the classification and the evidence without a fix.

The gate's verdict is in before the agent starts, and the agent never runs inside the gating job. The prompt points it at the two commands Playwright built for agents, covered with traces below. scripts/repair-agent.sh stands for whatever runs your coding agent; give its token permission to open draft PRs and nothing more. Currents' Fix failing CI tests closes the evidence gap the log leaves: it keeps everything about each failure, including the page's accessibility tree, screenshots, video and trace, whether the test is flaky, and the branch and commit where it started to fail, and gives it to the agent in one call through a Fix link that any agent able to open a URL can read.

When the healer skips

The healer re-runs a test until it passes or until guardrails stop the loop, and it returns either a passing test or a skipped test when it believes the functionality is broken. Treat that skip as a bug report, and never merge one automatically. A merged skip removes the check for exactly the feature the healer thinks is broken.

Classification stays with you and your own agent, not the healer. A feature request asked for a triage agent that runs before the healer, because the reporter worried that when the app really regressed, the healer could leave a test quietly adjusted to match the bug (#43675). The same reply added that you are probably better off asking your agent to fix the tests than using the healer at all.

Time and token budgets

An agent loop has no natural end, so the pipeline has to supply one. Playwright won't by default: globalTimeout, the maximum time in milliseconds the whole suite can run, defaults to zero, and zero disables it. Set it on every run an agent starts, and back it with a timeout on the CI job.

The per-test timeout needs the same attention. Playwright Test enforces 30 seconds per test by default, and that budget includes fixture setup and beforeEach hooks along with the test function. An agent-written test that logs in through a fixture and passes on a fast laptop can run out of that budget on a slower CI runner. Make both limits explicit:

import { defineConfig } from '@playwright/test';

// Set AGENT_RUN=1 in every job an agent starts
const agentRun = !!process.env.AGENT_RUN;

export default defineConfig({
  testDir: './tests',
  // Zero, the default, means no limit; agent runs get a hard ceiling
  globalTimeout: agentRun ? 15 * 60 * 1000 : 0,
  // Covers the test function, fixture setup and beforeEach hooks
  timeout: 30 * 1000,
});

Fifteen minutes is a placeholder: pick the ceiling from how long your suite normally takes.

The healer's own guardrails are not your CI budget. Cap attempts per failure and per run in whatever script starts the agent, and stop at the cap.

Every agent step also costs something on each run: time, tokens, variance in how long the run takes, and a failure that is harder to explain. Budget agent runs the way you budget CI minutes, per run and per PR.

Where tokens go

Snapshots and traces are where an agent's context fills up fastest. Hand the agent the failing test, its error and the one artifact that explains it, never whole directories.

Idle browsers

A headless browser launched by the Playwright MCP server closes after an hour without tool calls. An agent session that pauses longer than that, waiting on a human review for instance, loses the browser, and the next tool call launches a new one, so the agent has to navigate again.

Anti-pattern

Parallel healers on one server

A batch of failures lands, so someone starts several healer agents at once against the same MCP test server to clear it faster. One user ran three healer agents at once against one MCP test server, one per failing test (#39235). Only one finished; the other two hung. A maintainer replied that this is not a supported scenario. The issue was closed as not planned.

Fix

Run one healer per MCP test server, and work through failures in series under the per-run cap. A batch too big for that is too big to repair blind anyway: triage it first.

Debugging agent runs with traces

When an agent-written test fails, the trace is the evidence, for you and for the agent that tries to repair it. Keep it. With trace: 'retain-on-failure', Playwright records a trace for every run but keeps only the ones that failed, and it keeps a failed run's trace even when a later retry passes. The attempt that failed is the one worth explaining. A person reading it can follow our step-by-step trace viewer walkthrough for red CI jobs.

Two commands give a coding agent its own way in. npx playwright trace lets it explore a trace from the command line to understand a failing or flaky test. npx playwright test --debug=cli lets it attach to and debug tests over playwright-cli, which the release notes call perfect for automatically fixing tests in agentic workflows:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    // Keep a failed attempt's trace even when a later retry passes
    trace: 'retain-on-failure',
  },
});
# Open the failing test's trace, then list only its assertion actions
npx playwright trace open test-results/<test-dir>/trace.zip
npx playwright trace actions --grep="expect"
# Attach to the failing test while fixing it
npx playwright test tests/agent/checkout.spec.ts --debug=cli

Anti-pattern

The whole trace in the prompt

A repair job pastes the full CI log and the whole trace into the agent's context. A trace file is a zip of several megabytes, too large for an agent to read at once, so the dump burns the token budget and the agent still has to guess which part explains the failure.

Fix

Read a trace the way you'd read it yourself: the failing action and its error first, then only the part they point to. Currents' Explore a trace works for traces from CI tests and from agent-recorded sessions. The agent starts from a summary of the actions and the failure, then asks only for the page, request or screen the summary points to, and in the documented example it used about 1,600 tokens where every file in the trace together comes to about 20,000.

Judging agent-written tests

Judge agent-written specs on the same three numbers as the specs your team wrote: flake rate, retry count and duration. Specs that flake more, retry more or run longer cost more than they save, however fast an agent wrote them.

Anti-pattern

A green run as proof

A generated spec goes green in CI and gets counted as working. With retries enabled, Playwright retries a failing test until it passes or hits the maximum, so a green run can hide a first attempt that failed. Playwright marks that test flaky: it failed on the first run and passed on retry.

Fix

Make flaky results visible and fatal for agent-written specs. Retries are off by default, so turn them on in CI for flaky tests to show up as flaky. Set failOnFlakyTests so they fail the run. Then repeat each directory with --repeat-each before trusting what's in it:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  // Off by default; with retries on, a fail-then-pass is reported as flaky
  retries: process.env.CI ? 2 : 0,
  // Exit with an error when any test is marked flaky
  failOnFlakyTests: !!process.env.CI,
  use: { trace: 'retain-on-failure' },
});
CI=1 npx playwright test tests/agent --repeat-each=20
CI=1 npx playwright test tests/human --repeat-each=20

With retries on for both runs, the flaky counts for the two directories compare directly. To follow them over time, keep every run's report and compute a flake rate.

Isolated retries

When a spec flakes only inside the full suite, try the 'isolated' retry strategy. It runs retries at the end, after all other tests finish, one by one in a single worker, which cuts interference between retried tests and the rest of the suite at the cost of a longer run. A spec that still flakes in isolation is flaky on its own. One that passes there was colliding with something else in the suite.

Before generating more

Keep agent-written specs in their own directory while you roll agents in, so the numbers stay separate.

Then look past the green runs. Count how often the specs break on a UI state the plan didn't cover, or on a page object the generator didn't use. Check that every failure leaves a trace that explains it. Add the tokens and review time each spec took to produce to its run duration. Last, watch whether the team acts on a red result from these specs without re-running it first, because a result people re-run before they believe it hasn't earned their trust.

Settle each of those questions with evidence you can check yourself, starting with Playwright's own pass, fail and flaky results, and never with a second model reviewing the first.

When not to use agents

Keep the core regression gate fully specified. When a check has to give the same verdict on every commit, an agent adds cost and variance and nothing else.

Don't hand a real regression to the healer. Its skipped test is the bug report, as the repair section showed.

Prefer exact assertions to an agent's judgment. An agent that writes assertions from what it sees can freeze today's bug into the test: if the total is wrong when the agent looks, the wrong total becomes the expected value. A natural-language assertion checked by a model has the same weakness, since it passes whatever the model accepts.

Stop adding agents where a step's token and review cost is higher than the time it saves. The gate is where that happens first. Stop generating specs for an area, too, while its agent-written specs flake, retry or run longer than the ones your team wrote.

Anti-pattern

A fully agentic suite

A team tired of fixing selectors lets agents drive every test at runtime. Selector upkeep goes away, and so does any way to say what a passing run checked or to reproduce a failing one.

Fix

Keep deterministic specs in the gate, and use agents to author, repair and explore around it. The agents take on the slow work, and a red build means the same thing on every commit.

Frequently asked questions

What is agentic e2e testing?

In agentic testing, an agent decides the next step: it loops, reading the app's state, acting, and deciding again. In AI-assisted testing a human decides and a model suggests code, and in AI-generated testing a model writes the whole test in one shot.

What are Playwright test agents?

Playwright comes with three Test Agents out of the box: a planner, a generator and a healer. The generator transforms the Markdown plan into Playwright Test files, and the healer automatically repairs failing tests.

Should AI agents run in the CI merge gate?

No. The merge gate runs committed spec files and nothing else, and agents propose code in branches and PRs but never run inside the gating job. The --run-agents option accepts missing, all or none, and none is the default, as the deterministic merge gate section shows.

What does the Playwright healer agent do when it can't fix a test?

The healer returns either a passing test or a skipped test when it believes the functionality is broken. Treat that skip as a bug report, and never merge one automatically, because a merged skip removes the check for exactly the feature the healer thinks is broken, as When the healer skips explains.

How do I find out if agent-written Playwright tests are flaky?

Retries are off by default, so turn them on in CI for flaky tests to show up as flaky, and set failOnFlakyTests so they fail the run. Then repeat each directory with --repeat-each before trusting what's in it, as the green run anti-pattern shows.

Can an agent trust every element in a Playwright MCP snapshot?

No. Read a snapshot as a list of what exists on the page, not of what a user can reach.

What evidence does a repair agent need to fix a failing CI test?

Trigger it from a real CI failure, and give it more than the log. A CI log doesn't show what was on the page, which request returned the wrong data, or whether the test failed the same way last week, as the repair agents section explains.

How do I stop an agent run in CI from running forever?

Set globalTimeout on every run an agent starts, and back it with a timeout on the CI job. globalTimeout, the maximum time in milliseconds the whole suite can run, defaults to zero, and zero disables it, as Time and token budgets covers.