Write E2E tests in plain language. Replay them for $0.

The LLM plans the browser actions once. From the second run on, Windup does a deterministic replay with zero LLM calls~1 second, $0, stable results. You edit scenarios, not selectors.

$ npm i -D windupjs
  • No selectors, no test code
  • llm_calls=0 on replay
  • Playwright under the hood
windup run checkout
Terminal recording: a natural-language scenario runs; first run plans with llm_calls=1, the second run replays from cache with llm_calls=0 and $0
Real terminal — first run plans (llm_calls=1), the second run replays from cache at $0.

The problem

Hand-written E2E tests break every time a selector moves. AI agents that drive the browser on every run are slow, non-deterministic, and bill you an LLM call per test.

Windup plans once and caches a validated action plan. Replays are deterministic and free. When the app changes and a check fails, the plan is invalidated and re-planned automatically — self-healing, not silently wrong.

How it works

Plan once with the LLM, then replay without it.

The expensive part — figuring out the browser actions — happens a single time and is turned into cached, verifiable data.

  1. 1

    Plan (once)

    A natural-language task goes to the planner. The LLM emits a JSON action plan — one call, on the first run only.

    llm_calls=1 · ~$0.0025
  2. 2

    Execute + verify

    A deterministic executor (Playwright) runs the plan. Cheap DOM/URL post-conditions verify each action — no LLM.

    trusted input events
  3. 3

    Replay (free)

    The trajectory is cached. Every later run replays it: zero LLM calls, ~1 second, $0, same plan every time.

    llm_calls=0 · $0
  • Plans are data, not code — Schema-validated JSON — no runtime improvisation.
  • Verification without LLM — Post-conditions check DOM/URL on every action.
  • Self-healing cache — A failed check invalidates the plan → re-plan.
  • Zero hardcoded site knowledge — The engine knows nothing about your app up front.

New in 1.0

Author by demonstration windup record

Prefer showing over describing? windup record opens a headful browser at your app — you click the flow, mark what to verify, and finish. Windup writes the scenario and caches the recorded plan, so it replays deterministically at $0, no LLM.

npx windup record --url http://localhost:3000
1

Click the flow

A real browser opens at your app. Log in, navigate, fill forms — just use it.

2

Mark the verification

A floating toolbar sits at the bottom: hit ◉ then click the element the test should check — or mark nothing to verify the final URL.

3

Finish → $0 replay

“■ finalizar” (or Ctrl-C). Windup writes the scenario + caches the plan. windup run <id> replays it at $0.

  • A typed password never enters the plan — it’s stored in .env.local and referenced as an ENV value_ref.
  • Recorded selectors follow the engine’s own priority (#id → [data-testid] → [name] → type → role/text), with an accessible-name fallback.
  • A real UI change invalidates the cache? It self-heals by re-planning from the task, like any scenario.
How windup record works →

Why Windup

The reliability of scripts, the ergonomics of natural language.

Hand-written scripts are cheap to run but expensive to maintain. Per-run AI agents are easy to write but slow and non-deterministic. Windup takes the good half of each.

Dimension Hand-written scripts AI agent every run Windup
Authoring Code + selectors Natural language Natural language
Cost per run $0 LLM on every run LLM on the 1st run only
Speed Fast Slow (model in the loop) ~1s replay
Determinism High Low (improvises) High (same plan every time)
App changed You fix the script May do the wrong thing silently Check fails → re-plans

Features

Everything a QA workflow needs — minus the selectors.

A CLI built for real projects: authoring by prose or by demonstration, dependencies, dynamic values (OTP/magic-link), device emulation, performance budgets, request stubbing, runtime-health gates, CI guard-rails, read-only diagnostics, and reporters — everything a QA workflow needs.

Natural-language scenarios

Describe the test in plain language. No selectors, no page objects, no test code to maintain.

Plan once, replay free

From the 2nd run on: llm_calls=0, ~1s, $0. The cached plan runs the same way every time.

Deterministic execution

Playwright with trusted input events (isTrusted) — reliable clicks, typing and navigation.

Cheap verification

DOM/URL post-conditions checked on every action, with no LLM in the loop. Assert visible text, an element count, an attribute, or that something is gone — not just “a selector exists”.

windup record

Author by demonstration: drive a headful browser, mark the verification with a toolbar, finish — Windup writes the scenario and caches the recorded plan ($0 replay). A typed password never enters the plan.

windup trends / why / diff

Read-only diagnostics from the run ledger, no LLM: per-scenario pass-rate history, why a scenario re-plans, and what changed between two runs. Plus windup badge for a status SVG.

Device emulation

Run a scenario at a Playwright device preset with --device "iPhone 14" — viewport, UA, mobile/touch. Cached plans are keyed per device, so mobile and desktop are separate trajectories.

Web vitals + budgets

Capture the final page’s TTFB / FCP / LCP / CLS with --web-vitals, and fail the run when config.budgets is exceeded — a performance gate that rides on the run you already do.

config.network / config.clock

Stub a request (a 500, an empty list, a dropped call) and freeze the clock/timezone — per run, never cached. Scope them to a single scenario, so one error-state test doesn’t leak into every run.

Runtime health gates

Fail a scenario that logged a console error or got a silent 5xx during the run (--fail-on-console / --fail-on-5xx) — a run can’t “pass” while the page is broken underneath.

CI guard-rails

Retry a flake (--retries), cap the suite’s wall-clock (--max-wall), stop on first failure (--bail), or quarantine a known-flaky scenario so it reports without failing the build — surfaced, never hidden.

windup suggest-scenarios

From the routes with no test yet, the LLM drafts one scenario per uncovered route — closing the scan → coverage → author loop.

windup new

LLM-assisted authoring: writes the scenario from your app’s real screens (site map) + project manifest. --validate runs and refines until it passes.

windup scan

Indexes your routes and elements straight from the source (Next.js, react-router).

windup secret

Credentials never enter the scenario, cache, LLM prompt or git — only ENV:* references, resolved at runtime.

depends_on

Dependencies between scenarios (e.g. "create invoice" depends on "login") — same session, cached per dependency.

run --summary

The AI writes a post-run debrief quoting real observed values — prices, messages, confirmations.

run --suggest

On failure, the AI reads the real page and suggests the fix to your scenario.

CI reporters

JUnit, JSON and self-contained HTML (--reporter). Non-zero exit code on any failure.

Multi-provider

Google Gemini and OpenAI, picked per run (--llm openai:gpt-5-mini). windup costs tracks spend by provider and model.

run --concurrency

Run scenarios in parallel over one shared warm browser with isolated contexts — ~2× faster on a mixed suite, more with planning or long flows. Sequential by default.

Cross-browser

Run the same scenarios on Chromium (default), Firefox or WebKit with --browser. A single plan replays across all three — author once, run everywhere.

config.resolve

Dynamic values (OTP codes, magic-links) fetched at run time from an author-declared source (cmd/http/fn). A plan uses them via value_ref/url_ref — unblocks passwordless login. resolveFields binds a field deterministically.

Session snapshots

A dependency’s auth state (storageState) is captured once and restored on cached replays — so the login flow isn’t re-run for every dependent (deps≈0). The big suite speed-up.

config.seed

Inject localStorage/sessionStorage before a plan runs — reach a cart or a POS-device state directly, with no server round-trip. Deterministic and CI-safe.

config.forbid

Safety denylist: a run that targets a forbidden selector or URL aborts — the CI guardrail against changing the test password, deleting data, or persisting config.

windup coverage

Cross-references the routes windup scan indexed with your scenarios and lists the routes that have no test yet — coverage gaps, found automatically.

windup doctor

Preflight before CI: LLM key, browser, scenarios parse, no orphaned fragments, site map scanned — catches the common "it’ll break in CI" problems first.

run --a11y

A free accessibility audit (axe-core) on each scenario’s final page — violations reported. Informational; never fails the run.

run --all --shard

Round-robin-split the suite across parallel CI runners (--shard 1/4, 2/4, …), each a separate job.

run --all --changed

Incremental CI: run only the scenarios a code or scenario change affects (git diff → route attribution), with a safe full-suite fallback — never a silent false green.

readySignals

Reusable anti-flake readiness per route glob: wait for the app to be ready before acting, defined once instead of repeated as a hint in every scenario.

Example

A scenario is natural language in JSON. Two runs tell the whole story.

You write the intent. The first run plans and pays a few tenths of a cent; every run after that is a cached replay at $0.

checkout.json scenario
{
  "scenario_id": "checkout",
  "task": "Log in as the qa account, add 'Backpack' to the cart, check out and verify the order confirmation message appears."
}
windup run checkout output
$ windup run checkout      # 1st run
PASS  checkout  cache=miss llm_calls=1 total=3003ms cost=$0.0024

$ windup run checkout      # again — deterministic replay
PASS  checkout  cache=hit  llm_calls=0 total=671ms  cost=$0

cache=hit · llm_calls=0 · $0 — the second run never touches the model.

CI / CD

Drop it into your pipeline. Exit codes and reports included.

Run the whole suite in one warm browser, fail the build on any failing scenario, and emit machine- or human-readable reports.

ci — one line bash
$ npx windup run --all --reporter junit \
    --report-file reports/windup.xml
  • --all runs every scenario in a single warm browser for the whole suite.
  • --concurrency <n> runs scenarios in parallel (~2× faster on a mixed suite); --browser firefox|webkit to run cross-browser.
  • Non-zero exit code when any scenario fails.
  • --reporter junit|json|html; windup costs --json tracks AI spend in the pipeline.

Reliability

Deterministic means deterministic.

Replays are measured, not promised. The cached plan produces the same result on every run — no model, no flakes.

60/60
cached replays passed
4 scenarios × 15 replays — zero flakes
llm_calls=0
on every replay
login · multi-step checkout · add/remove · a 2nd site
~$0.0025
per generation
default model, gemini-3.1-flash-lite
106 routes
production dogfood
run against a real production app
Windup mascot holding a green check mark

Get started

Five commands from zero to a replaying test.

Node ≥ 20 and one API key — GOOGLE_GENERATIVE_AI_API_KEY (Google, default) or OPENAI_API_KEY (OpenAI) — in .env.local. Keys are used only for planning; cached replays never call an LLM.

Windup mascot running
$ npm i -D windupjs
Chromium is provisioned automatically
$ npx windup init
3 questions → windup.config.ts
$ npx windup scan
indexes your app’s routes from the source
$ npx windup new "log in and create an invoice for ACME"
LLM-assisted authoring
$ npx windup run checkout
1st run plans · then replay ~1s, $0
Node ≥ 20Google Gemini or OpenAIPlaywright bundledMIT

For the AI era

Let your AI wire up the tests.

Nobody reads docs anymore — they hand them to an assistant. So Windup ships an llms.txt: the whole documentation, structured for machines. Point your coding agent at it, describe the flows in plain words, and it authors and runs the scenarios for you.

1

Give your AI the docs

Paste this URL into your assistant (Claude, Cursor, Copilot, …).

https://windup.run/llms.txt
2

Describe what to test

A ready-to-paste starting prompt — fill in your flows.

Read https://windup.run/llms.txt and set up Windup E2E tests for my app, then write scenarios for these flows: ...