# Playbooks

Part of the Oya Browser docs. All of it: https://oyabrowser.com/docs.md

## Playbooks

A playbook is a task the agent did once, kept as Playwright steps. Replaying it runs those steps with no model in the loop, so it is fast, costs no tokens and does the same thing every time.

```
await browser.ask("Check eligibility for member {{memberId}}", {
  data: { memberId: "W2847-1193" },
});
const playbook = await browser.toPlaybook("eligibility-check");

// Later, on every case:
const result = await browser.play("eligibility-check", { memberId: "W5512-0042" });
// { steps: 7, total: 7, fellBack: false }
```

In the console the same loop is a button: run a prompt, then press **Save as playbook**.

### Variables and secrets

Put values in the prompt as `{{name}}`. Anything in `data` the agent can read; anything in `secrets` it never sees. Either way it types them through the placeholder, so the saved playbook keeps names, not values.

```
await browser.ask("Sign in as {{user}} with {{password}}, then open claims", {
  data: { user: "ops@clinic.example" },
  secrets: { password: process.env.PORTAL_PASSWORD },
});
```

`playbook.variables` lists the inputs and `playbook.defaults` what the run used, so `play(name)` with nothing repeats it and you pass only what changes. A secret has no default: it never left the page.

### Free-text fields

A comment box or a question to answer should not be the same text every run. Oya spots these fields and leaves them to your model, one short call per field, listed in `playbook.answers`. The generated code reads:

```
await page.getByLabel("Reason for visit").fill(
  vars["reason"] ?? (await oya.llm.answer("Reason for visit", vars)),
);
```

Pass the field's key in `play()` to type fixed text instead. A playbook without such a field makes no model call at all.

### When a page changes

If a step no longer fits the page, the agent finishes the task from there and its fix replaces the broken steps, so the next replay runs clean. The result says `healed: true`. Pass `{ autoHeal: false }` to throw the step's error instead.

### Signing in on replay

A replay that lands on a login page signs in with the persona's stored login and second factor, then carries on. Store them once; they are sealed at rest and never read back. See MFA for the code types.

```
await oya.personas.setCredentials(personaId, {
  domain: "portal.payer.example",
  username: "ops@clinic.example",
  password: process.env.PORTAL_PASSWORD,
});
await oya.personas.setMfa(personaId, { type: "totp", secret: process.env.PORTAL_TOTP });
```

### The Playwright code

`playbook.code` is the same flow as a Playwright module you own. Read it in review, keep it in git, or run it on any Playwright page:

```
// playbook.code, saved as eligibility-check.js:
// export default async function run(page, vars, oya) { ... }
import run from "./eligibility-check.js";

await run(page, { memberId: "W5512-0042" }, oya);
```

### Export and import

Move a playbook from staging to production, or from the cloud to your own deployment. Secrets travel by name only; set their values where it lands.

```
const doc = await staging.playbooks.export("eligibility-check");
await production.playbooks.import(doc, { overwrite: true });
```

```
oya playbooks export eligibility-check --out eligibility-check.json
oya playbooks import eligibility-check.json --replace
```

### Background runs

For long queues, submit a replay and hear back. `onHumanAttention` fires for a CAPTCHA or code nobody could solve; the run waits until you `respond()`.

```
await browser.submit({ playbook: "eligibility-check" }, {
  data: { memberId: "W5512-0042" },
  onSuccess: (result) => save(result),
  onHealed: (result) => notify("the portal changed; the playbook was fixed"),
  onHumanAttention: async (request) => { /* finish by hand */ await request.respond("done"); },
});
```

| Method | Endpoint | What it does |
| --- | --- | --- |
| POST | `/api/browsers/:id/playbooks` | Save the browser’s last run: { name } |
| POST | `/api/browsers/:id/playbooks/:name/play` | Replay: { variables, autoHeal } |
| GET | `/api/playbooks` | Every saved playbook |
| GET | `/api/playbooks/:name/export` | One playbook as a JSON document |
| POST | `/api/playbooks/import` | Save an export: { playbook, name?, overwrite? } |
| DELETE | `/api/playbooks/:name` | Delete a playbook |

Every endpoint takes the API key as a bearer token, like the rest of the REST API.
