agents· 17 min read· by Pramod Dutta

Natural-Language Test Automation, Explained

A from-scratch guide to natural language test automation: write tests as plain-English objectives, watch an AI agent drive a real browser, and keep it honest.

Natural language test automation means you describe what a test should do in plain English, and an AI agent drives a real browser to make it happen. No CSS selectors. No page objects. No waiting on a locator that broke because a designer renamed a class. You write "Open the pricing page and confirm the Pro plan shows $49 per month," and an agent reads the page, decides where to click, types where it needs to, and hands you back a verdict. This article walks through how that actually works under the hood, why the naive version of it is untrustworthy, and where deterministic checks come back in to make the whole thing safe to put in CI.

I have spent a lot of time in the traditional world of Playwright and Selenium, so I want to be honest from the start: plain English is not magic, and an agent that "figures it out" is exactly as dangerous as it sounds unless you constrain it. The interesting part of natural language test automation is not the demo where it clicks a button. It is the engineering that makes a green run mean something.

What natural language test automation actually is

Start with the mental model. In a classic end-to-end test you write imperative code: find this element by this selector, click it, assert that the URL changed. You are the compiler. You translate human intent ("log in") into machine steps (fill #email, fill #password, click button[type=submit], wait for /dashboard). Every one of those selectors is a promise about the DOM that the app can break at any time.

Natural language test automation moves that translation step from you to a model. You keep the intent, the model produces the steps. Your test file says:

An agent reads the live page, builds an internal picture of what is on screen, and chooses the actions. The selectors still exist somewhere, but they are computed at run time against the page as it is right now, not hardcoded against the page as it was six months ago when you wrote the test. That is the core value proposition, and it is real.

BrowserBash is one open-source take on this idea. It is a free, Apache-2.0 CLI from The Testing Academy that you install with a single npm command and run against real Chrome. You give it a plain-English objective, it drives the browser step by step, and it returns a deterministic verdict plus structured results you can feed into CI or into another AI agent. I will use it as the concrete example throughout, because "explaining natural language test automation" in the abstract is a great way to say nothing.

npm install -g browserbash-cli
browserbash run "Open https://example.com and confirm the heading says Example Domain"

How the agent maps intent to actions

Here is the loop that turns a sentence into browser actions. It is worth understanding because every quirk of natural language testing comes out of this loop.

Step one: perceive the page

The agent cannot "see" pixels the way you do (and even when it can, that is expensive and slow). Instead it takes a structured snapshot of the page: the accessibility tree, visible text, roles, labels, form fields, links, and buttons. This snapshot is the agent's entire world. If a button has no accessible name and no visible text, the agent has a hard time with it, the same way a screen-reader user would. That is not a bug so much as a mirror. Pages that are easy for an agent to test are usually pages that are more accessible in the first place.

Step two: plan the next action

The agent takes your objective plus the current snapshot and asks the model: given that I want to accomplish this, and this is what is on screen, what is the single next action? The answer is one concrete tool call: navigate to a URL, click the element with this role and name, type this text into this field, wait for something to appear, or extract a value. In BrowserBash's builtin engine these are literally named tools (navigate, snapshot, click, type_text, wait_for, extract, done) that the model is allowed to call, so the model's freedom is bounded by a small, auditable vocabulary.

Step three: act, then re-perceive

The agent performs the action against the real browser, then takes a fresh snapshot. The page changed, so its picture of the world updates. It loops: perceive, plan, act, perceive. This continues until the objective is met or a stop condition (timeout, max steps, an explicit done) fires. The agent then emits a verdict.

That loop is why natural language tests can absorb small UI changes that would shatter a selector-based test. A renamed button still has the same accessible name and role, so "click the Sign in button" keeps working. A field that moved elsewhere in the form is still "the email field." The agent re-derives the path every run. It is also why the naive version is untrustworthy, which brings us to the whole point of this article.

Why plain English alone is not enough

An agent that judges its own success is a problem. If you write "confirm the order went through" and let the model decide whether it went through, you have handed your pass/fail verdict to a probabilistic system that wants to be helpful. Models are agreeable. Under ambiguity they lean toward "looks good to me." That is exactly the wrong bias for a test, whose entire job is to be a skeptical gatekeeper.

There are three specific failure modes to worry about:

  1. Hallucinated success. The agent reports PASS because the page superficially resembles a success state, even though the real success signal (a confirmation number, a specific URL, a total) is absent or wrong.
  2. Non-determinism. Run the same natural-language check twice and get two different judgments because the model sampled differently or read the page differently. A test that flips its own verdict is worse than no test.
  3. Silent scope drift. The agent "helpfully" completes a flow a slightly different way than you intended, papering over a real bug because it found an alternate path to something that looks like the goal.

None of these are reasons to abandon natural language test automation. They are reasons to not let the model be the judge of anything that matters. The intent-to-action mapping is a great job for a model. The pass/fail decision is a terrible one. So you split them.

Where deterministic Verify steps keep it trustworthy

This is the load-bearing idea, so I want to be precise. In BrowserBash you write a committable Markdown test file (a *_test.md), and inside it a Verify line is not judged by the model at all. It compiles to a real Playwright assertion. When you write Verify the URL contains /dashboard, that becomes an actual Playwright URL check. A pass means the condition literally held. A fail comes back with expected-versus-actual evidence in the structured run_end.assertions block and in a human-readable Result.md assertion table.

The grammar covers the checks you reach for most:

Anything inside that grammar runs deterministically, with zero LLM judgment. If you write a Verify line that falls outside the grammar, BrowserBash does not silently drop it. It still runs the check, but agent-judged, and flags it with judged: true in the results so you can see at a glance which assertions were deterministic and which leaned on the model. That distinction is the whole trust story: you always know whether a green check was a fact or an opinion.

So a healthy natural-language test looks like this. The agent handles the fuzzy navigation ("get me to the order confirmation"), and deterministic Verify steps nail down the facts that define success ("the URL contains /orders/, the text Order confirmed is visible, the stored total equals $49.00"). The agent gets you there. The assertions decide whether "there" is correct. You keep the flexibility of English and the rigor of a real assertion library at the same time.

A worked example: seeding data, then checking the UI

Deterministic checks get more powerful once you can also set up state deterministically. That is what testmd v2 adds. Put version: 2 in a test file's frontmatter and the steps stop running as one blob and start running one at a time against a single browser session. Two step types never touch a model at all: API steps and Verify steps.

API steps let you hit your backend directly to seed data, in the same file, in plain-ish text:

---
version: 2
---

# New user sees their name on the dashboard

- POST https://api.example.com/test/users with body {"name": "Ada Lovelace"}
- Expect status 201, store $.token as 'token'
- Open https://example.com/login
- Sign in using the token {{token}}
- Verify text "Ada Lovelace" is visible
- Verify the URL contains /dashboard

The POST and Expect status lines are deterministic HTTP, no model involved, so you create a known user with a known name. Then the plain-English steps drive the UI as an agent, and the Verify lines check the result deterministically. Consecutive English steps run as grouped agent blocks on the same page, so you are not paying for a fresh browser or a fresh reasoning context on every line. Data setup is exact, UI navigation is flexible, and the checks are facts. That is the shape of a natural-language test you can actually trust in CI.

Two honest caveats. First, testmd v2 currently drives the builtin engine, which speaks the Anthropic API, so you need an ANTHROPIC_API_KEY or a compatible ANTHROPIC_BASE_URL gateway to run v2 files. It does not yet run directly on Ollama or OpenRouter. Second, v1 files (no frontmatter) behave exactly as they always did, so nothing you already wrote breaks when v2 ships.

Running models locally, and when to reach for a hosted one

A fair question about any AI-driven tool is "what is it sending where, and what does it cost." BrowserBash is Ollama-first. By default it resolves a local Ollama model, so no API keys, no network egress, nothing leaves your machine. If you do not have Ollama, it falls back in order to ANTHROPIC_API_KEY, then OPENAI_API_KEY, then OpenRouter, and only errors with setup guidance if it finds nothing usable.

Here is the honest part, because it matters for whether your tests pass reliably. Very small local models (roughly 8B parameters and under) get flaky on long, multi-step objectives. They lose the thread, forget a sub-goal, or misread a busy page. For short flows they are fine and delightfully free. For hard, multi-page journeys the sweet spot is a mid-size local model in the Qwen3 or Llama 3.3 70B class, or a capable hosted model. This is not a knock on local inference, it is just where the capability line sits today. Pick the model to match the difficulty of the flow, not the other way around, and you will save yourself a lot of confusing red.

There are cost controls when you do use hosted models. Every run's run_end carries a cost_usd estimate from a bundled per-model price table (unknown models get no estimate rather than a made-up one). And you can cap a whole suite:

browserbash run-all .browserbash/tests --budget-usd 2.50 --shard 2/4

Once the suite crosses the budget, BrowserBash stops launching new tests, reports the remaining ones as skipped, and exits with code 2. The spend lands in the suite's Result file and in the JUnit properties so your CI has a record. The --shard 2/4 flag runs a deterministic slice computed on sorted discovery order, so four parallel CI machines split the work without needing to coordinate.

Natural language versus selector-based testing

Let me put the two approaches side by side honestly, because natural language testing is not strictly better. It is a different set of tradeoffs.

Dimension Selector-based (Playwright/Selenium) Natural language (agent-driven)
Who writes the steps You, in code You describe intent, agent derives steps
Reaction to UI change Breaks on selector change Absorbs many changes via roles and accessible names
Determinism of navigation Fully deterministic Model-driven, varies unless cached
Determinism of assertions Deterministic (expect) Deterministic only via Verify; otherwise judged
Authoring speed Slower, needs selectors Faster, plain English
Debuggability Stack trace to a line Verdict plus step events and evidence
Cost per run CPU only CPU only if local, tokens if hosted
Best at High-churn critical paths where exactness is everything Broad coverage, smoke tests, flows that change often

The takeaway is not "switch everything." It is that the assertion column should always be deterministic wherever the outcome matters, and natural language test automation gives you that column through Verify while buying you speed and resilience in the navigation column. If you have a hyper-critical checkout flow where you want a hand-tuned selector on every element, a classic Playwright spec is still a completely reasonable choice, and BrowserBash can even import it (more on that below) to give you a plain-English starting point.

Making runs cheap and stable: the replay cache

There is one more piece that makes natural-language tests practical rather than a party trick: the replay cache. The first time a test passes, BrowserBash records the actions the agent took. The next identical run replays those recorded actions with zero model calls. The agent only steps back in when the page actually changed from what was recorded. In effect, the first green run compiles your English into a concrete action script, and subsequent runs execute that script for free, healing only on drift.

This changes the economics of two things. Monitoring becomes nearly token-free, so an always-on synthetic monitor is cheap:

browserbash monitor ./login_test.md --every 10m --notify https://hooks.slack.com/services/XXX

Monitor mode runs on an interval and alerts only on state changes, both directions, so you get pinged when a passing flow starts failing and again when it recovers, never on every green run. Slack webhook URLs get Slack formatting automatically. And because the replay cache carries the heavy lifting, the recurring cost of watching a flow all day is close to nothing. Secrets never enter the cache in plaintext, and secret-marked variables are masked as ***** in every log line, which matters when you are recording login flows.

Fitting into an agent-driven workflow with MCP

If you are building with AI coding agents, the most interesting angle is that BrowserBash speaks the Model Context Protocol. Run browserbash mcp and the CLI serves itself over MCP on stdio, so Claude Code, Cursor, Windsurf, Codex, or Zed can call it as a native tool. One line wires it up:

claude mcp add browserbash -- browserbash mcp

It exposes three tools: run_objective for a single plain-English objective, run_test_file for a *_test.md file, and run_suite for a whole folder in parallel. Each returns the structured verdict JSON: status, summary, final_state, assertions, cost_usd, duration_ms. The crucial design decision is that a failed test is a successful tool call. The MCP call succeeds and hands the agent a verdict to read, rather than throwing an error the agent has to interpret. That is the "validation layer for AI agents" idea in practice: your coding agent writes a change, then calls BrowserBash to actually check it in a real browser, and reads back a structured yes or no. BrowserBash is listed on the official MCP Registry as io.github.PramodDutta/browserbash if you want the canonical entry.

For plain CI that is not agent-driven, use --agent to get NDJSON on stdout (one JSON event per line, with step and run_end events) and rely on the frozen exit codes: 0 passed, 1 failed, 2 error or infra or budget-stop, 3 timeout. No prose parsing, no scraping logs. There is also an official GitHub Action that installs the CLI, runs your suite, uploads JUnit, NDJSON, and result artifacts, supports shard matrix jobs and a budget cap, and posts a self-updating PR comment with a verdict table. The GitHub Action docs cover the setup.

Getting from zero to a suite without writing selectors

Two features lower the starting cost of natural language test automation to almost nothing.

If you already have Playwright specs, browserbash import converts them to plain-English *_test.md files heuristically and, importantly, with no model involved, so the conversion is deterministic and reproducible. It handles goto, click, fill, press, check, selectOption, getBy* locators, and common expects, and turns process.env.X into {{X}} variables. Anything it cannot translate is written to an IMPORT-REPORT.md rather than being silently dropped or invented, so you know exactly what needs a human look.

browserbash import ./tests/e2e

If you are starting from scratch, browserbash record <url> opens a visible browser. You click through the flow once, hit Ctrl-C, and it writes a plain-English test. Password fields never leave the page: the capture script sends only a secret marker, so the generated step reads Type {{password}} into ... rather than leaking a credential into your test file. Record once, and you have English you can commit, diff, and re-run forever. The tutorials walk through both paths end to end, and the learn hub goes deeper on the testmd format.

Who this is for, and who should think twice

Natural language test automation is a strong fit if you have flows that change often and a team that spends too much time repairing selectors, if you want AI coding agents to self-validate their work in a real browser, if you need broad smoke coverage fast, or if you want free local runs with the option to escalate hard flows to a capable model. It is also a good fit if you value committable, human-readable tests that a product manager can read and a new hire can understand without learning your selector conventions.

Think twice in a few cases. If every test targets a single ultra-critical path where you want pixel-exact control over every element, hand-written Playwright may serve you better, though you can still lean on deterministic Verify steps to get most of the way. If you are stuck on a tiny local model, expect flakiness on long journeys and keep objectives short. And if your app is genuinely inaccessible, with unlabeled controls and text baked into images, an agent will struggle for the same reasons a real user on assistive tech would, and the honest fix is to improve the accessibility rather than fight the tool.

The through-line of all of it is the same principle we started with. Let the model do the fuzzy, resilient part, mapping your English intent onto whatever the page looks like today. Never let the model be the judge of whether the test passed. Put deterministic Verify steps on every outcome that matters, read the assertions block, and treat a judged: true flag as a note to tighten that check. Do that, and a plain-English suite is not a demo. It is a real gate you can put in front of a deploy.

FAQ

What is natural language test automation?

Natural language test automation is the practice of writing browser tests as plain-English objectives instead of code with selectors. An AI agent reads the live page, decides which actions to take, and drives a real browser to accomplish your stated intent. The key to using it safely is pairing the agent's flexible navigation with deterministic assertions so the pass or fail verdict is a checked fact rather than a model's opinion.

How does an AI agent turn plain English into browser actions?

The agent runs a perceive-plan-act loop. It takes a structured snapshot of the page (roles, text, labels, and form fields), asks the model for the single next action given your objective, performs that action against the real browser, then snapshots again and repeats until the goal is met or a stop condition fires. Because it re-derives the path from the current page every run, small UI changes like a renamed button usually do not break the test.

Can natural language tests be trusted in CI?

Yes, as long as the outcomes are checked deterministically rather than judged by the model. In BrowserBash, Verify steps compile to real Playwright assertions and return expected-versus-actual evidence, so a green check means the condition literally held. Combined with frozen exit codes, NDJSON output, and structured verdicts, that makes natural-language suites safe to gate a deploy on.

Do I need API keys or paid models to run natural language tests?

No. BrowserBash is Ollama-first and defaults to a free local model with no API keys and no data leaving your machine. It falls back to Anthropic, OpenAI, or OpenRouter keys only if you have them, and hosted usage comes with a cost estimate and suite budget caps. The one tradeoff is that very small local models can be flaky on long multi-step flows, so hard journeys do better on a mid-size local model or a capable hosted one.

Get started

Natural language test automation is worth trying on a flow you already have, especially one that keeps breaking your selectors. Install the CLI with npm install -g browserbash-cli, record or import a flow, add a couple of deterministic Verify lines, and run it. Everything runs locally and free by default, and an account is optional if you ever want the hosted dashboard. When you are ready, sign up here or just read more on the blog.

Try it on your own appnpm install -g browserbash-cli
Start learning