Skip to main content

Prowl for Agents

Prowl is CLI-first and agent-native by design. A single CLI command can capture an entire hunt run. Structured JSON output, deterministic exit codes, and minimal context overhead give AI agents what they need to discover, run, and report on browser tests without wrangling automation APIs directly.

Made for agents, controlled by humans. You write the hunts, review the results, and set the guardrails. Agents handle the execution loop — discovering what to test, running the hunts, and reporting structured results back to your workflow.

Jump to the Library API for programmatic Node.js integration, the MCP Server to connect an agent without shell access, or Using Starter Templates for the hunts the CLI bundles.

Quickstart in 60 Seconds​

Get an agent running hunts in four commands:

# 1. Install
npm install -g prowl-tools

# 2. Initialize a project
prowl init

# 3. Run a hunt with structured output
prowl run smoke-test --json
{
"hunt": "smoke-test",
"status": "passed",
"steps": 4,
"duration": "1.2s"
}
# 4. Branch on the result
prowl run smoke-test --json && echo "✅ Passed" || echo "❌ Failed"

Exit codes make branching simple: 0 = passed, 1 = failed, 2 = no hunts found. Run all hunts at once with prowl ci --json.

tip

That's the full loop — install, run, branch. The sections below cover the details: workflow patterns, the Node.js library API, CI integration, and the CLI's starter templates.

Why CLI-First​

Prowl is designed around the same interface agents already know: the shell. A single CLI command replaces multi-step tool orchestration, and structured output means agents spend less time parsing and more time acting.

Minimal Context Overhead​

  • No schema preamble. The CLI is invisible to the agent's context until invoked — no manifest or tool definitions sitting in every turn.
  • Declarative, not imperative. A 10-step hunt is a single prowl run call — not 10+ individual tool invocations the agent has to reason through.
  • Pay-per-use. Tokens are only spent when the agent actually calls a command and reads its output.

Exit Codes Enable Branching​

Agents don't need to parse response bodies to decide what to do next. A zero exit code means pass, non-zero means fail — standard shell semantics that every agent framework already understands.

prowl ci --json && echo "All hunts passed" || echo "Failures detected"
tip

Prowl is agent-ready out of the box. Install, run, parse — structured JSON output and deterministic exit codes from the first command.

Agent Workflow: Discover, Run, Report​

Every agent integration follows the same three-step pattern — each step is a single CLI call with structured output:

1. Discover​

List available hunts and their metadata:

prowl list --json
[
{ "name": "smoke-test", "description": "Validates homepage loads", "tags": ["smoke"] },
{ "name": "login-flow", "description": "Email/password login", "tags": ["auth", "critical"] },
{ "name": "checkout-flow", "description": "E-commerce checkout", "tags": ["e2e"] }
]

Agents can filter by tags to select the right hunts for the context (e.g., run only smoke hunts on every PR, run e2e hunts nightly).

2. Run​

Execute a single hunt or all hunts:

# Single hunt
prowl run smoke-test --json

# All hunts (CI mode)
prowl ci --json

3. Report​

Parse the structured JSON output and check exit codes:

Exit CodeMeaning
0All hunts passed
1One or more hunts failed
2No hunts found or all skipped

CLI Integration (--json flags)​

prowl run <hunt> --json​

Returns a single hunt result:

{
"status": "pass",
"exitCode": 0,
"startedAt": "2026-02-16T17:15:12.481Z",
"hunt": "smoke-test",
"targetUrl": "http://localhost:3000",
"durationMs": 220,
"steps": [
{ "type": "navigate", "status": "pass", "durationMs": 120 },
{ "type": "wait", "status": "pass", "durationMs": 85, "selector": "text=\"Welcome\"" },
{ "type": "assert", "status": "pass", "durationMs": 15, "value": "visible:Sign In" }
],
"assertions": [
{ "type": "noConsoleErrors", "value": true, "status": "pass" },
{ "type": "noNetworkErrors", "value": true, "status": "pass" }
],
"artifacts": {
"summary": "summary.md",
"screenshots": ["screenshots/final.png"],
"console": "console.log"
}
}

On a retried run (a hunt with a retry block where more than one attempt ran), the result also carries retryHistory and retrySummary (0.1.10). Both are absent on a first-attempt pass and on any artifact written before 0.1.10, so existing parsers are unaffected:

{
"retryHistory": [
{ "attempt": 1, "status": "fail", "durationMs": 5010, "failedStep": { "index": 0, "type": "navigate" }, "error": "Timeout 5000ms exceeded" },
{ "attempt": 2, "status": "pass", "durationMs": 240 }
],
"retrySummary": "Passed on attempt 2 of 3 — first failure: navigate (Timeout 5000ms exceeded)"
}

Each retryHistory record has a 1-based attempt (attempt 1 is the initial run), a status, its durationMs, and — on a failed attempt only — the first failedStep ({ index, type }, index 0-based) and the error. The last entry always matches the final result. See Retry diagnostics.

prowl ci --json​

Returns a combined result for all hunts:

{
"status": "fail",
"startedAt": "2026-02-16T17:15:12.481Z",
"durationMs": 4870,
"totalHunts": 3,
"passed": 2,
"failed": 1,
"skipped": 0,
"hunts": [
{ "hunt": "smoke-test", "status": "pass", "durationMs": 220, "runDir": "/path/to/.prowl/runs/2026-02-16_17-15-15" },
{ "hunt": "login-flow", "status": "pass", "durationMs": 1450, "runDir": "/path/to/.prowl/runs/2026-02-16_17-15-20" },
{ "hunt": "checkout-flow", "status": "fail", "durationMs": 3200, "error": "Assertion failed: visible \"Order Confirmed\"" }
]
}

JUnit XML​

For CI systems that consume JUnit XML (GitHub Actions, GitLab CI, Jenkins, CircleCI), pass --junit:

prowl ci --junit

Each hunt run directory gets its own junit.xml (for example .prowl/runs/<timestamp>/junit.xml).
In CI mode, prowl ci also writes a combined summary JSON file at .prowl/runs/ci-<timestamp>/ci-result.json.

prowl history <hunt> --json​

Every prowl run and prowl ci appends to .prowl/history.json. Agents can read past runs to detect flakes or track duration trends:

prowl history smoke-test --json --limit 50
[
{ "hunt": "smoke-test", "status": "pass", "durationMs": 1240, "startedAt": "2026-05-28T17:15:12.481Z", "runDir": ".prowl/runs/2026-05-28_17-15-12-481", "retries": 1 },
{ "hunt": "smoke-test", "status": "fail", "durationMs": 1840, "startedAt": "2026-05-28T09:02:55.001Z", "runDir": ".prowl/runs/2026-05-28_09-02-55-001" }
]

Each entry carries an optional retries count (0.1.10) — retry attempts beyond the first — omitted when zero; older entries without it still parse. Retention is configurable via history.maxRuns — see Configuration.

Connect Over MCP​

For agents that support the Model Context Protocol, Prowl ships an MCP server that exposes QA as named tools (list_hunts, run_hunt, run_suite, list_projects) over stdio — no shell access required. The agent connects once and drives runs through tool calls, with run_suite optionally logging failures straight to your backlog.

See the MCP Server guide for client configuration, the tool reference, automated bug-logging, and multi-project setup.

Library API (Node.js)​

For deeper integration, import Prowl as a library:

import { runHunt, loadConfig, listHunts, loadHunt } from "prowl-tools";

Core Functions​

FunctionDescription
runHunt({ huntName, ...options })Run a hunt programmatically and get a typed RunResult
runSuite(options?)Run an entire suite (the engine behind prowl ci); returns aggregated CiResult
updateBacklogFromSuite(suiteResult, options?)Log a suite's failures as deduplicated bug tickets in docs/backlog.md
readHistory(configDir) / readHuntHistory(configDir, hunt)Read run history from .prowl/history.json
loadConfig(configPath?)Load and validate config, returning { config, configPath, configDir }
loadHunt(huntName, configDir)Load and parse a single hunt file from <configDir>/hunts/
listHunts(configDir)List available hunt names from <configDir>/hunts/

Schemas​

Prowl exports Zod schemas for validation:

import { huntSchema, configSchema, stepSchema } from "prowl-tools";

// Validate a hunt file an agent generated
const result = huntSchema.safeParse(agentGeneratedHunt);
if (!result.success) {
console.error(result.error.issues);
}

Variable Interpolation​

import { loadConfig, loadHunt, interpolateHunt } from "prowl-tools";

const { configDir } = loadConfig();
const hunt = loadHunt("login-flow", configDir);
const { hunt: interpolated } = interpolateHunt(hunt, {
...process.env,
EMAIL: "test@example.com",
PASSWORD: process.env.TEST_PASSWORD ?? "",
});

Suite Runner and Automated Bug-Logging​

runSuite() runs an entire hunt suite programmatically — the same engine behind prowl ci — and updateBacklogFromSuite() turns its failures into deduplicated bug tickets. This is the non-CLI path to the same automated bug-logging the MCP run_suite tool performs.

import { runSuite, updateBacklogFromSuite } from "prowl-tools";

const suite = await runSuite({
includeTags: ["smoke"],
parallel: 4,
});

console.log(`${suite.result.passed}/${suite.result.totalHunts} passed`);

// Log failures to docs/backlog.md (and detect regressions against docs/resolved.md)
const summary = updateBacklogFromSuite(suite, { projectRoot: process.cwd() });

console.log("New tickets:", summary.created); // e.g. ["QA-014"]
console.log("Regressions:", summary.regressions); // e.g. ["QA-015"]
console.log("Already open:", summary.skipped); // left untouched

runSuite(options?) returns Promise<{ result: CiResult; resultPath: string | null }>. Key options:

OptionTypeDescription
configPathstringPath to config (defaults to discovery from cwd)
urlOverridestringOverride target.url
includeTags / excludeTagsstring[]Tag filters
parallelnumberNumber of concurrent workers
junitbooleanEmit junit.xml per run
headed, slowMo, trace, browser, channel, viewport—Same semantics as the matching CLI flags
hooksRunSuiteHooksProgress callbacks: onHuntStart, onStep, onHuntSuccess, onHuntFailure, onHuntSkipped

updateBacklogFromSuite(suiteResult, options?) returns a BugLogSummary — { created, regressions, skipped, backlogPath }. A failure is fingerprinted by hunt + failing step (type/selector) + normalized error, so new failures get a QA-NNN ticket under a ## QA Findings (automated) section, already-open failures are skipped, and failures matching a resolved ticket are logged as regressions. Options:

OptionTypeDefaultDescription
projectRootstringprocess.cwd()Project root containing docs/
backlogPathstring<projectRoot>/docs/backlog.mdOverride the backlog path
resolvedPathstring<projectRoot>/docs/resolved.mdOverride the resolved-tickets path
datestringtoday (YYYY-MM-DD)Date stamp for new tickets

Run History​

readHistory() and readHuntHistory() read the .prowl/history.json log written by every run and ci:

import { loadConfig, readHistory, readHuntHistory } from "prowl-tools";

const { configDir } = loadConfig();

const all = readHistory(configDir); // { entries: HistoryEntry[] }
const smoke = readHuntHistory(configDir, "smoke-test"); // HistoryEntry[]

const failRate = smoke.filter((e) => e.status === "fail").length / smoke.length;

HistoryEntry is { hunt: string; status: "pass" | "fail"; durationMs: number; startedAt: string; runDir?: string; retries?: number } (retries added in 0.1.10 — retry attempts beyond the first, omitted when zero). Retention is capped per hunt by history.maxRuns — see Configuration.

Using Starter Templates​

Prowl ships starter hunts inside the CLI. Running prowl init scaffolds a .prowl/ directory with four ready-to-edit hunts — hello.yml (a minimal smoke test), login-flow.yml (a commented auth example), form.yml (a forms example), and macos-hello.yml (a desktop-first macOS starter, experimental) — so an agent has a concrete pattern to copy without fetching from any external registry.

Manual Workflow​

  1. Run prowl init to scaffold the starter hunts.
  2. Copy a starter to a new file in .prowl/hunts/ — the file name is the hunt's identity.
  3. Customize {{VAR}} placeholders and selectors for your environment.
  4. Execute the hunt with prowl run <hunt-name> --json.

Example hunt after copying and customizing:

name: login-flow
description: Email/password login with error handling
steps:
- navigate: "{{BASE_URL}}/login"
- fill:
selector: "[name='email']"
value: "{{EMAIL}}"
- fill:
selector: "[name='password']"
value: "{{PASSWORD}}"
- click: "button[type='submit']"
- wait: "Welcome"

Variables are resolved from your config, environment, or CLI flags. See Variables for interpolation precedence.

Generating hunts from scratch​

An agent can also author hunt YAML directly and validate it with the exported huntSchema before running — no template needed. See Schemas above.

Library API for Templates​

Use the library API to customize and run a starter (or any hunt) programmatically:

import { loadConfig, loadHunt, interpolateHunt, runHunt } from "prowl-tools";

const { configPath, configDir } = loadConfig();

// Load the template
const hunt = loadHunt("login-flow", configDir);

// Preview interpolation with custom variables
const { hunt: preview } = interpolateHunt(hunt, {
...process.env,
BASE_URL: "https://staging.example.com",
EMAIL: "test@example.com",
PASSWORD: process.env.TEST_PASSWORD ?? "",
});

console.log(preview.steps[0]);

// runHunt reads vars from process.env
process.env.BASE_URL = "https://staging.example.com";
process.env.EMAIL = "test@example.com";
process.env.PASSWORD = process.env.TEST_PASSWORD ?? "";

const { result } = await runHunt({
huntName: "login-flow",
configPath,
});

if (result.status === "pass") {
console.log(`Login flow passed in ${result.durationMs}ms`);
} else {
console.error("Login flow failed:", result.steps.filter((s) => s.status === "fail"));
}
note

The starter hunts ship inside the CLI. Contribute your own by opening a pull request to the prowl repository — see Starter Templates.