Stagehand, but with Jev.
Natural-language browser automation for Playwright, powered by Jev.
Documentation · Quickstart · API · Evals
PlayJev is a TypeScript library for controlling websites with plain-English instructions while keeping the full Playwright API. Give it an existing Playwright page, then ask it to click controls, navigate websites, fill forms, inspect page state, or choose between visible options.
If you know Stagehand, the shortest explanation is: PlayJev is Stagehand, but with Jev making the browser decisions instead of a generative LLM.
await page.act("Open the pricing page");
await page.act("Complete the contact form", { fields });
await page.check("Did the form submit successfully?");Important
PlayJev is a new, experimental project—not a production-stable release. The API is actively evolving as it is tested against broader browser benchmarks. It currently requires Chromium/CDP and access to the TypeSafe Jev API.
The silent 48-second recording plays above and shows PlayJev running public Browserbase/Stagehand evaluation pages: selecting checkboxes, operating a custom dropdown, filling a cross-origin iframe form, and navigating across multiple pages. Click it or open the video directly for the higher-quality MP4.
You need Node.js 20 or newer and a TypeSafe Jev API key. Sign in to the TypeSafe console to get access to Jev, then install PlayJev and Playwright:
npm install @filed/playjev playwright
npx playwright install chromiumSet the required key in your environment:
export TYPESAFE_API_KEY="your-key-here"Then use the Quickstart below. To run the included example from source instead:
git clone https://github.com/filedcom/playjev.git
cd playjev
npm install
cp .env.example .env
npm run exampleThe example opens example.com, asks Jev to identify and follow the explanatory link, and verifies that the destination page loaded.
import { chromium } from "playwright";
import { playjev } from "@filed/playjev";
const browser = await chromium.launch();
const page = playjev(await browser.newPage());
await page.goto("https://example.com");
const action = await page.act("Open the documentation link");
const loaded = await page.check("Is the documentation page open?");
console.log({ action: action.success, loaded });
await browser.close();The wrapped object still exposes Playwright's page and locator APIs:
await page.getByRole("button", { name: "Sign in" }).click();
await page.act("Choose the workspace with the most recent activity");
await page.screenshot({ path: "workspace.png" });PlayJev's semantic page.check(question) replaces Playwright's legacy page.check(selector) shorthand. Use page.locator(selector).check() for deterministic checkbox interaction.
PlayJev keeps the control loop bounded: Playwright owns execution; Jev makes decisions. Jev selects from real browser nodes and a fixed operation vocabulary instead of generating selectors, JavaScript, or arbitrary action objects.
- No raw HTML trees sent to Jev
- No generated selectors, JavaScript, or arbitrary action JSON
- No silent LLM fallback
- Playwright locators, navigation, events, screenshots, and assertions stay available
- Shadow DOM, cross-origin iframes, styled controls, and multiple tabs covered by public evals
- Bulk forms use one batched target-selection request and one verification
Ask a yes-or-no question and receive both the answer and probability.
const paid = await page.check("Is this invoice marked paid?");
// { answer: true, probability: 0.97 }Choose only among possibilities defined by your code.
const plan = await page.choose("Which plan is selected?", {
starter: "Starter is selected",
pro: "Pro is selected",
enterprise: "Enterprise is selected",
});Place browser state on an explicit ordered scale.
const completion = await page.rate("How complete is this form?", [
"Empty",
"Partially complete",
"Complete",
]);Score targets, choose a node and operation, execute it through Playwright, then verify the outcome.
const result = await page.act("Click the Medium pizza-size option", {
minTargetConfidence: 0.7,
minVerificationProbability: 0.6,
});Caller-supplied values stay separate from natural-language instructions:
await page.act("Fill the search box", { value: "climate attribution" });
await page.act("Press Enter in the search box", { key: "Enter" });PlayJev can match and fill a whole form in one target-selection round trip:
await page.act("Complete the contact form", {
fields: {
"First Name": "Nunya",
"Last Name": "Business",
Email: "test@example.com",
"Preferred Contact Method": "Phone",
Message: "Hello from PlayJev",
},
});Every value comes from the caller. Jev selects controls; deterministic code performs fills, checks, unchecks, and native option selection.
┌──────────────────┐ ┌────────────────────┐ ┌─────────────────┐
│ Playwright page │ ──▶ │ Sparse YAML state │ ──▶ │ Jev decisions │
│ AX + frame nodes │ │ numbered + scoped │ │ Score / Choice │
└──────────────────┘ └────────────────────┘ └────────┬────────┘
│
┌──────────────────┐ ┌────────────────────┐ │
│ Verified result │ ◀── │ Playwright action │ ◀────────────┘
│ fresh snapshot │ │ fixed vocabulary │
└──────────────────┘ └────────────────────┘
Model-facing state looks like this:
page:
url: https://example.test/contact
title: Contact
nodes:
- n: 4
parent: 2
type: textbox
text: First Name
actionable: true
- n: 5
parent: 2
type: radio
text: Phone
state:
- checked:false
actionable: trueInternal selectors and browser node IDs never appear in that YAML. They remain in the local snapshot map used to resolve Jev's numbered choice.
The repository includes outcome-based browser evaluations adapted from public Browserbase Stagehand fixtures, plus a fixed-answer smoke suite drawn from the official WebVoyager dataset.
| Evaluation | What it covers | Result |
|---|---|---|
| Checkboxes | Labelled native checks | ✅ |
| Styled radio | Pointer-intercepting control | ✅ |
| Hidden input | Dynamic reveal and fill | ✅ |
| Custom dropdown | Multi-step interaction | ✅ |
| Shadow DOM | Semantic target resolution | ✅ |
| Cross-origin iframe | Five independent fields | ✅ |
| Bulk iframe form | Five controls, batched selection | ✅ |
| Multiple tabs | Popup adoption and navigation | ✅ |
Latest complete local run: 8/8 passed. On that run, the bulk iframe form completed in 0.91s, versus 7.41s for five separate actions. These are observed timings, not guaranteed benchmarks.
The latest WebVoyager golden smoke run passed 4/4 live tasks across Cambridge Dictionary, ArXiv, GitHub, and Wolfram Alpha. Each case requires exact evidence from the live page and a positive Jev check(). This is an integration gate using official tasks, not a score on the full 643-task benchmark.
npm run eval:stagehand
EVAL_FILTER=bulk npm run eval:stagehand
npm run eval:webvoyager:goldenplayjev/
├── src/
│ ├── api/ # Playwright Page integration
│ ├── browser/ # capture and sparse browser normalization
│ ├── core/ # Jev-driven control loop
│ ├── errors/ # public error classes
│ ├── jev/ # TypeSafe API client
│ ├── serialization/ # model-facing YAML
│ └── types/ # public TypeScript contracts
├── docs/ # VitePress docs and landing page
├── evals/stagehand/ # public browser outcome evaluations
├── evals/webvoyager/ # official fixed-answer golden smoke
├── examples/ # focused runnable examples
└── tests/unit/ # deterministic unit tests
npm install
npm run validate # types, unit tests, package build, docs build
npm run docs:dev # local documentation site
npm run eval:stagehand # live Jev + browser evaluations
npm run eval:webvoyager:golden # official WebVoyager golden smokeSee CONTRIBUTING.md for the development workflow and SECURITY.md for private vulnerability reporting.
PlayJev is built on Playwright and Jev by TypeSafe. Its API design is inspired by the clarity of Stagehand, but the decision architecture and public primitives are Jev-native. PlayJev is independent and is not affiliated with Browserbase or Stagehand.

