Turn any AI browser run into clean, production Playwright code.
recast is a compiler that takes AI browser agent recordings and emits clean, readable, static Playwright test code - with no LLM required at replay time, no proprietary runtime dependencies, and no magic.
workflow-use JSON ──┐
HAR file ──┤ recast compile ──▶ playwright.spec.ts
CDP event log ──┤ ──▶ .env.example
MCP tool call log ──┘
AI browser agents (workflow-use, browser-use, Skyvern, Operator) re-reason through browser tasks at runtime — burning LLM tokens on every step, every run, forever. Recorded workflows are locked to proprietary runtimes and can't be committed to CI pipelines, reviewed by developers, or diff-ed.
recast fills the gap: compile once, run forever, on plain Playwright.
$ recast compile login_workflow.json
recast: INFO detected format: workflow-use JSON
recast: INFO parsed 5 steps
recast: INFO [dedup] removed 0 duplicate steps
recast: INFO [selector] hardened 2 selector(s)
recast: INFO [waits] injected 3 explicit wait(s)
recast: INFO [sanitize] 2 credential(s) replaced with environment variables
recast: INFO [branch] no conditional branches detected
recast: INFO [emit] wrote playwright-ts to ./recast-out/test_login_workflow.spec.ts
recast: INFO [emit] wrote .env.example to ./recast-out/.env.example
$ cat ./recast-out/test_login_workflow.spec.ts
import { test, expect } from '@playwright/test';
test('login_workflow', async ({ page }) => {
await page.goto('https://app.example.com/login');
await page.waitForLoadState('networkidle');
await page.locator('#email').fill(process.env.TEST_EMAIL!);
await page.locator('#password').fill(process.env.TEST_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForNavigation({ timeout: 10000 });
await page.locator('.dashboard').waitFor({ state: 'visible', timeout: 30000 });
});Download a pre-built binary (no Go required):
# macOS arm64 (Apple Silicon)
curl -L https://github.com/yagna-1/recast/releases/latest/download/recast_darwin_arm64.tar.gz | tar xz
sudo mv recast /usr/local/bin/
# macOS x86_64 (Intel)
curl -L https://github.com/yagna-1/recast/releases/latest/download/recast_darwin_x86_64.tar.gz | tar xz
sudo mv recast /usr/local/bin/
# Linux x86_64
curl -L https://github.com/yagna-1/recast/releases/latest/download/recast_linux_x86_64.tar.gz | tar xz
sudo mv recast /usr/local/bin/Or grab the .zip for Windows from the releases page.
Go install:
go install github.com/yagna-1/recast/cmd/recast@latestBuild from source:
git clone https://github.com/yagna-1/recast
cd recast
go mod tidy
make build
./bin/recast version# Compile a workflow-use JSON recording to Playwright TypeScript
recast compile workflow.json
# Output:
# recast: INFO detected format: workflow-use JSON
# recast: INFO parsed 6 steps
# recast: INFO [sanitize] 2 credential(s) replaced with environment variables
# recast: INFO [emit] wrote playwright-ts to ./recast-out/test_login_and_download.spec.ts
# recast: INFO [emit] wrote .env.example to ./recast-out/.env.example
# Exit code can be 1 when warnings are present (partial success).
# View output
cat ./recast-out/test_login_and_download.spec.tsCompile a workflow to Playwright test code.
recast compile [flags] <input-file>
Flags:
-o, --output string Output directory (default "./recast-out/")
-t, --target string playwright-ts | playwright-py | ir-json (default "playwright-ts")
--no-optimize Skip optimizer passes
--no-harden Skip selector hardening
--inject-assertions Add post-action assertions
--replay-exact Preserve recorded sequence; disable behavior-changing heuristic passes
--strict Exit 2 on any warning
-v, --verbose Detailed output
Examples:
recast compile workflow.json
recast compile workflow.json -t playwright-py -o ./tests/
recast compile trace.har --inject-assertions
recast compile mcp-log.jsonl -t ir-jsonValidate an input file without compiling.
recast validate workflow.jsonVerify compiled Playwright output against the original trace using screenshot-free signals (URL + key selectors).
recast verify ./recast-out/test_login_and_download.spec.ts --against workflow.json
# optional runtime check (no screenshot diff)
recast verify ./recast-out/test_login_and_download.spec.ts --against workflow.json --runtime
# strict runtime mode (fails if runtime cannot execute or test fails)
recast verify ./recast-out/test_login_and_download.spec.ts --against workflow.json --runtime --runtime-strict --runtime-timeout-sec 120Notes:
- If the provided test file path is slightly off,
recast verifyauto-resolves to the closest*.spec.ts/*.spec.pyfile in the same directory and prints a warning. - Runtime verification injects safe placeholder defaults for sanitized variables (
TEST_EMAIL,TEST_PASSWORD,RECAST_VAR_1..10) when unset, so strict checks can run in clean environments.
List all supported input formats and output targets.
Print version information.
0success1partial success with warnings2hard validation/compilation failure3input file not found/unreadable4unsupported input/output format
Parses input from supported formats into a normalized Intermediate Representation (IR). The IR is the AST — every optimization and code generation step operates on it.
Supported input formats:
- workflow-use JSON (
{"workflow_name": ..., "steps": [...]}) - HAR (HTTP Archive) — exported from any browser DevTools
- CDP Event Log — raw Chrome DevTools Protocol logs
- MCP Tool Call Log — from agents using MCP browser tools
| Pass | Name | Default |
|---|---|---|
| 1 | Deduplication — remove consecutive identical actions | ✓ |
| 2 | Selector stabilization — upgrade fragile CSS to ARIA/role | ✓ |
| 3 | Wait inference — inject explicit waits | ✓ |
| 4 | Credential sanitization — replace secrets with env vars | ✓ always |
| 5 | Branch detection — flag missing conditional branches | ✓ |
| 6 | Assertion injection — add post-action assertions | opt-in |
Generates clean, readable test code in the target language.
Supported targets:
playwright-ts— Playwright TypeScript (.spec.ts)playwright-py— Playwright Python (.py)ir-json— Normalized IR JSON (for debugging / community tools)
Input (workflow.json):
{
"workflow_name": "login_and_download",
"steps": [
{ "type": "navigate", "url": "https://app.example.com/login" },
{ "type": "fill", "selector": "#email", "value": "user@example.com" },
{ "type": "fill", "selector": "#password", "value": "hunter2" },
{ "type": "click", "selector": "button[type=submit]" },
{ "type": "wait_for", "selector": ".dashboard" }
]
}Output (test_login_and_download.spec.ts):
import { test, expect } from '@playwright/test';
test('login_and_download', async ({ page }) => {
// Navigate to login page
await page.goto('https://app.example.com/login');
await page.waitForLoadState('networkidle');
// the email input field
await page.locator('#email').fill(process.env.TEST_EMAIL!);
// the password input field
await page.locator('#password').fill(process.env.TEST_PASSWORD!);
// the Sign in button
await page.locator('button[type=submit]').click();
await page.waitForNavigation({ timeout: 10000 });
await page.locator('.dashboard').waitFor({ state: 'visible', timeout: 30000 });
});.env.example:
# Generated by recast — fill in actual values before running tests
# DO NOT commit this file with real values
# step_002: fill value was sanitized (detected: email pattern)
TEST_EMAIL=
# step_003: fill value was sanitized (detected: high-entropy string)
TEST_PASSWORD=Credential sanitization runs always and cannot be disabled. Any fill step value that matches an email pattern, common credential field name, JWT token, credit card number, or high-entropy string is automatically replaced with an environment variable reference.
The .env.example file lists all sanitized variables. Fill in the real values locally; never commit them.
recast is structured identically to a programming language compiler:
Input Formats → Ingestion Adapters → IR (AST) → Optimizer Passes → Emitters → Output
Adding a new input format = write one ingestion adapter. Adding a new output target = write one emitter. Optimizer passes run on IR regardless of input or output.
The IR package (recast-ir) is published as a standalone Go module so agent frameworks can emit IR directly — bypassing file-based ingestion entirely.
git clone https://github.com/yagna-1/recast
cd recast
go mod tidy # fetch cobra, viper, testify (populates go.sum)
make test # all unit tests
make build # compile binary
make golden-update # generate golden files on first run
make golden-test # run golden output tests (requires golden-update first)GitHub Actions runs:
go mod tidyconsistency checkgofmtformat checkgo vetmake testmake golden-testmake build
See .github/workflows/ci.yml.
See CONTRIBUTING.md for development workflow and PR expectations.
First-time contributor? Look for issues labeled good first issue — each one has a step-by-step guide.
recast/
├── cmd/recast/ # CLI entrypoint
├── recast-ir/ # IR type definitions, builder, validator, marshaler
├── internal/
│ ├── ingestion/ # Format adapters (workflow-use, HAR, CDP, MCP)
│ ├── optimizer/ # Passes 1-6
│ ├── emitter/ # Playwright TS, Python emitters
│ └── config/ # Config management
├── pkg/recast/ # Public Go library API
└── testdata/ # Fixtures and golden files
This project follows the Contributor Covenant Code of Conduct. By participating, you agree to uphold these standards.
MIT — see LICENSE.