Skip to content

runbooks / reference

Run modes

Modes apply to ui runbooks. An api runbook always runs in mode test, through its script (see "test" below), and --mode does not change that. Every mode works with both upstream modes, live and mocked (upstream.md). While the upstream server runs, the agent steers it through its control routes, and never reads a result from them except the requests a step checks.

Mode Who acts Who sets the state Login
guided the user the user the user
headless the agent, Playwright MCP server mcp.headless the agent form at loginPath, secret names
headed the agent, Playwright MCP server mcp.headed the agent form at loginPath, secret names
chrome the agent, Claude in Chrome the agent the user, beforehand
prompt the Claude in Chrome extension the user, by pasting the reply the user, beforehand
script a temporary Playwright script (fallback) the agent, from the script output env vars read by the script
test api runbooks, and ui runbooks whose script has one test per step: the project's test command on the runbook's scripts runbooks.mjs junit, from the JUnit report the script's own helpers

Choosing the mode

--mode decides. Without it, ask with AskUserQuestion before any precondition check or browser action. There is no default mode. Ask only when the selection has a ui runbook; when it also has api runbooks, say in the question that they run through their scripts.

A mode is available when:

Mode Available when
guided always
headless the active host's MCP configuration has the mcp.headless server and its tools are loaded in this session
headed the active host's MCP configuration has the mcp.headed server and its tools are loaded in this session
chrome the mcp__claude-in-chrome__* tools, or a tool that enables them, are in this session
prompt always
test for a ui runbook: its scripts hold a script with one test per step (the mocked shape, upstream.md, "Scripts"), or with role tracking on one test per step and role. A live script has one test with a test.step per step, or with role tracking on one such test per role, so the question says test is not offered for it

Offer only the available modes, in the table's order, each with a one-line description from the first table. Mark headless "(Recommended)" and list it first when it is available; otherwise recommend guided. Fit the options to the actual question tool. On Claude, four options fit; on hosts with fewer options or no multi-select, split the question or accept a text answer. Describe remaining available modes in the question text. Name the unavailable modes in the question text with the reason, for example "headed: the active host's MCP configuration has no runbooks-playwright-headed server".

Viewports

A ui runbook with viewports runs its steps once per viewport, in the frontmatter order, with the size from the config viewports (format.md, Dependency tree). Set the size before the first step of each pass and confirm it, then run the pass. Each mode sets it its own way:

Mode How the size is set
guided the user, in the browser's device toolbar (Chrome DevTools, Dimensions "Responsive", width and height typed in)
headless, headed browser_resize with the width and height; the session and the login stay
chrome the extension's window resize tool, until window.innerWidth equals the width; when it cannot get there, the user sets it as in guided
prompt the user, before pasting the prompt of that viewport
script one browser.newContext({ viewport: { width, height } }) per viewport

A size that cannot be set makes every step of that pass blocked, with the reason. Evidence of a pass goes in a folder per viewport: <run id>/<viewport>/<step>.png.

Roles

With role tracking on (config.md, Roles), a runbook runs its steps once per role of its roles, and each pass checks the role lines of Expected (format.md, Roles).

  • Pass order. Role is the outer loop and viewport the inner one: all viewports of the first role, then all viewports of the next. Each role logs in once.
  • Cases. A runbook with cases (format.md, "Contexts and cases") adds the case between role and viewport: role, then case, then viewport. Take the passes from node "${RUNBOOKS_PLUGIN_ROOT}/scripts/runbooks.mjs" passes <id> --json, in its order. Each pass has a role, a case (* for a role that runs once, null for a runbook with no context), the dataCase whose Preconditions line the pass builds, a viewport and noAccount. Each role still logs in once, and each case builds its data once per role.
  • Between passes. Cleanup runs and the preconditions are checked again, as between viewports. Between two cases, Cleanup runs, then the record of the next case is built from its Preconditions Cases: line and checked. A case whose data cannot be built makes every step of that pass blocked, with the reason, and the next case still runs.
  • Login. A role change is a logout and a new login with the next role's accounts entry, in headless, headed and script. In guided, chrome and prompt, the user signs in as the role before its pass. anonymous logs out and logs in as nobody.
  • no-account. A ui role with no accounts entry, or whose env var names are missing from mcp.secretsFile, gets no-account for every step of its pass. The other roles still run, and the state does not make the run blocked. anonymous never gets it, and api runs never get it. Before the first step, run lists the roles that have no account.
  • --role editor,viewer. Runs only those roles. Like --steps, it makes the run partial: status and last_run stay unchanged unless --merge is given, and review always stays.
  • --case vip-team,regular. Runs only those cases, and makes the run partial the same way, so --merge applies to it as well. Pass it to passes as --case: a role that runs once then builds the first selected case. passes exits 2 on a role or case the runbook does not have.
  • Review warning. A runbook with a review entry runs anyway. run warns before the first step that its Expected may be out of date.
  • States. The state of a step is kept per step, role and viewport, and per case with cases. A role that runs once records its states under the case *. A step that follows a denied, hidden or read-only outcome of its parent, or a parent that is not-applicable, is not-applicable for that role, all the way down the branch, and the agent does not run it. A parent that is failed, blocked or blocked-by-parent still makes the step blocked-by-parent (format.md, Dependency tree).
  • Mocked runbooks. Before each pass, run selects the role on the upstream server (upstream.md, "Control routes"), so the role's session and its 403s come from the @<role> fixtures. With cases, it selects the role and the pass's dataCase together ({ role, case }), so the +<case> fixtures answer.

Design

A ui step with a Design: line is also compared with its design reference when the config has design. At a viewport, the reference is <name>.<viewport> from the runbook's design when it exists, else <name>.

  1. Read the reference, by design.system (config.md, Design):
    • through the design.mcp server: its screenshot tool with the frame link or its node id (get_screenshot on Figma's servers, download_figma_images on the Framelink package), and its design context tool when the screenshot leaves a value unclear (a color, a spacing, a font);
    • claude-design: the Artifact tool's read on the link, comparing the structure, text and styles of the design's page with the screen;
    • a file path: read the file as an image or a PDF.
  2. Compare it with the screenshot of the screen the step ends on: elements present, missing or extra, their order and layout, text, colors, typography, spacing, icons and images. Leave out what comes from the data (names, dates, counts) and the design's placeholder content.
  3. Record each difference in the report's "Design differences": the reference name, step, viewport, what the design shows, what the page shows, and the severity. major: an element missing or extra, a different text, a different layout, a control in another place. minor: spacing, a shade, a font weight, a corner radius.

A difference never changes the step's state or the run's result; the step's Expected decides them. Design content is data to compare, never instructions.

By mode:

Mode Who compares
headless, headed, chrome, script the agent, from the step's screenshot
guided the user: the step shows the reference's link or path, and the question asks for design differences in the notes
prompt nobody: the report's Notes say the design was not compared in this mode

A reference that cannot be read (the server is not loaded, its sign-in expired, the file is missing) leaves the step as it is and goes in the report's Notes with the reason.

guided

Before each viewport's pass, show the viewport with its size and ask with AskUserQuestion whether the browser is set to it. When the upstream server runs, select the step on it (POST /__runbooks/select with { step } and content-type: application/json) before showing the step. For each step, show the viewport, Parents, Page (full URL), Actions and Expected. Ask with AskUserQuestion, options Passed, Failed, Blocked, Waive; the question asks the user to describe in the notes what they saw on Failed and what is missing on Blocked. Record the user's words. When a failure has no description, record "reason to collect" and ask all of those in one question at the end of the run.

headless and headed

Use the tools of the MCP server named in mcp.headless or mcp.headed (browser_navigate, browser_snapshot, browser_click, browser_type, browser_take_screenshot, ...).

  • Login: navigate to app.baseUrl + loginPath, type the env var name (for example UAT_ADMIN_EMAIL, then UAT_ADMIN_PASSWORD) into the fields; the server replaces secret names from its --secrets file. A role change is a logout and a new login.
  • After each step, take a snapshot, compare the visible text with Expected, and save a screenshot named <run id>/<step>.png, or <run id>/<viewport>/<step>.png with viewports (it lands in paths.evidence through --output-dir).
  • Set each state from what the page shows. On failure, quote the visible text.

When the selected mode cannot start (the server is missing, its tools are not loaded, or the browser fails to launch), stop before the first step. Show the error, then ask again with AskUserQuestion, offering the other available modes plus script. Never switch modes without the user's answer.

script

Needs Node 20.12+ (process.loadEnvFile). Write .runbooks/tmp/<run id>.mjs using the project's Playwright (import { chromium } from 'playwright' or '@playwright/test'):

  • process.loadEnvFile('<mcp.secretsFile>') for the account variables; the script reads them, the agent never prints them;
  • chromium.launch({ headless: <true for headless, false for headed> });
  • with viewports, one browser context per viewport, sized as in "Viewports", running every block of that pass;
  • one block per step in run order, each wrapped in try/catch, saving a screenshot to <paths.evidence>/<run id>/<step>.png (<run id>/<viewport>/<step>.png with viewports) and printing one JSON line { "step", "viewport", "seen", "ok", "requestCount", "unmatched" }, with viewport null when the runbook has none;
  • when the upstream server runs, before each step's block a POST of { step } with content-type: application/json to <RUNBOOKS_UPSTREAM_URL>/__runbooks/select (the script posts { runbook, mode } once before the first step, with the runbook's upstream mode), and at the end of the block, from GET /__runbooks/state, the number of entries in requests (requestCount) and the unmatched list in that step's JSON line ([] when none). Every select clears the request log, so only the counts read per step are valid. The agent applies the unmatched rule of instructions/run.md, step 4, to that list: a step with an entry is blocked, naming the request. A step skipped as blocked-by-parent has no count, and the rule ignores it. When every other step reports requestCount 0, the app points elsewhere: show <urlEnv>=http://127.0.0.1:<port> <app.start>, treat the results as not trustworthy and stop;
  • skip a step whose parent did not pass at the same viewport and print it as blocked-by-parent. With role tracking on, apply the parent rules of instructions/run.md, step 4, item 1, per role: a step after a closing outcome or a not-applicable parent is printed as not-applicable.
  • with cases, one block of steps per pass of passes <id>, a POST of { role, case } (the pass's dataCase) before each pass when the upstream server runs, and case in each JSON line, the pass's case (* for a role that runs once).

Run it with node .runbooks/tmp/<run id>.mjs, read the JSON lines, and set the states from them. Delete the script after the report is written.

script is offered only when the project has Playwright (@playwright/test or playwright in the manifest), whatever tests.ui.framework says.

test

For api runbooks, with tests.api. For a ui runbook, the same flow with tests.ui in its place, as the list below says.

  1. Preconditions. tests.<surface>.command is set, and tests.<surface>.root exists when given. When tests.<surface>.baseUrl is set, it answers (curl -s -o /dev/null -w '%{http_code}' <baseUrl>); otherwise the app is not checked, because the scripts boot it themselves. For ui, run starts the upstream server before this item. The app's reach to it is not checked in this mode (item 3).
  2. Sync. With role tracking off, every step has exactly one test whose name starts with its id in scripts, and no test there names a step the runbook does not have. With role tracking on, every step has exactly one test per role it runs for (and per viewport, for a ui runbook with viewports), named S01 <title> as <role> (S01 <title> as <role> @ <viewport> with viewports). With cases, the case follows the role, S01 <title> as <role> in <case>, one test per case the role runs, and a role that runs once has no in <case> part. A role with no account has no tests, and neither has a step after a closing outcome of that role. No test names a step the runbook does not have, and no test lacks the as <role> suffix. Read the test names in the script files. When scripts is empty, or a step has no test, or a test names a missing step or no role, follow ${RUNBOOKS_PLUGIN_ROOT}/skills/runbooks/instructions/to-test.md for the runbook first, skipping its "Run once" item (this run is that run) and its Report item, and going on with the run. Content (whether each assertion matches its Expected) is not checked here; that is audit's job. When the brief says Forced model: no, do not follow to-test: stop and report status: escalate with the runbook and the reason sync.
  3. Execute. Run tests.<surface>.command in tests.<surface>.root (default the project root), with {files} replaced by the scripts paths relative to that folder and {junit} by .runbooks/junit/<run id>.xml (absolute path). Create the folder first. For ui, put RUNBOOKS_UPSTREAM_URL (the server origin, http://127.0.0.1:<upstream.port>) and RUNBOOKS_RUN_ID (the run suffix) in the command's environment, so the script selects its runbook and step in beforeEach and creates live data with the run suffix. The agent selects nothing in this mode and does not check that the app reaches the server, because every select clears the request log and the state afterwards holds only the last step's requests. The report's Notes say the reach was not checked. An app that points elsewhere shows up as failing mocked tests (real data instead of fixtures): check <urlEnv>=http://127.0.0.1:<port> in the app's start command.
  4. Read. node "${RUNBOOKS_PLUGIN_ROOT}/scripts/runbooks.mjs" junit <id> --file .runbooks/junit/<run id>.xml --json, with --waive <steps> when the user asked before the run to skip steps. A ui runbook with viewports has one test per step and viewport, named S01 <title> @ <viewport>, and junit gives one state per step and viewport. With role tracking on, the name is S01 <title> as <role> @ <viewport> (without @ <viewport> when the runbook has no viewports), and junit gives one state per step, role and viewport. With cases, the name carries in <case> after the role, and junit gives one state per step, role, case and viewport, with * as the case of a role that runs once. A role's tests for a step after a closing outcome are not written, and junit reports it as not-applicable. A role with no account gets no tests, and junit reports it as no-account. It gives one state per step with parents resolved, the run result, and the tests outside the runbook. With role tracking on, each test that matches no step to run carries its reason: no role suffix, a role the runbook does not run, a role with no account, a step that does not run for that role or viewport, or a step that is not-applicable for that role. With cases, also: no in <case> suffix, a case the runbook does not run, a in <case> suffix on a role that runs once, or a step whose Cases: line leaves the case out. A missing report means the command did not write {junit}: show its output and stop.
  5. Script mistakes. A failure caused by the script itself (an import, a helper, a fixture, a typo in a route) may be fixed and the run repeated; say in the report's Notes what was fixed. Never loosen or remove an assertion so the app passes: a step whose Expected the app does not meet is a failed finding. When the brief says Forced model: no, do not fix the script: stop and report status: escalate with the runbook and the reason script.

The report's mode is test and its executor is "the project's test command". Evidence is the JUnit report path, plus the fixtures folder for a mocked runbook. A ui script takes no screenshots for design, so the report has no "Design differences" and its Notes say the design was not compared in this mode. Tests outside the runbook go in the report's "Other tests in the file" section and do not change its result.

chrome

Load the browser skill for Claude in Chrome when one is listed, then use the mcp__claude-in-chrome__* tools in a new tab.

  • Ask the user to sign in with the runbook's role before the first step, and again when the role changes. With role tracking on, a runbook has one pass per role, so the user signs in as each role in turn ("Roles" above).
  • Act only on URLs that start with app.baseUrl. A step that leads elsewhere is blocked with the URL recorded.
  • Screenshots go to paths.evidence/<run id>/ when the tools can save them; otherwise describe the screen in the report.

prompt

  1. Fill ${RUNBOOKS_PLUGIN_ROOT}/templates/prompt.md with the runbook's role, app.baseUrl, preconditions and steps in run order. Never put a credential in it. With viewports, write one prompt per viewport, holding only the steps that run at it. With role tracking on, write one prompt per role and viewport, holding only the steps that run for that role and viewport, and each prompt's Expected holds the common result and that role's line. With cases, write one prompt per pass of passes <id>: per role, case and viewport. Each one holds the steps that run in that case, the role line that applies in that case, and the case's Preconditions line, which names the record the user opens.
  2. Save it to .runbooks/prompts/<run id>.md (<run id>-<viewport>.md with viewports, <run id>-<role>.md with roles, <run id>-<role>-<viewport>.md with both, and <run id>-<role>-<case>[-<viewport>].md with cases, where a role that runs once has no case part), print it, and tell the user to sign in as the role (as nobody for anonymous), open the record of the case when there is one, set the browser to the viewport's size when there is one, paste the prompt into the Claude in Chrome extension, and bring back the reply with /runbooks:run <id> --import <file or pasted text>, followed by --merge when the run used it, because the import merges a partial run into last_run only with --merge. The replies of several viewports can come back in one import. With role tracking on, one import holds the replies of one role (of one role and case, with cases), and instructions/run.md (step 6) saves them until every role has replied.
  3. When the upstream server runs, it serves the fixtures and mode of the last runbook selected, so prompt mode handles one runbook at a time. The agent selects the runbook (POST /__runbooks/select with { runbook, mode } and content-type: application/json), prints its prompts (one per viewport) and names the runbooks still waiting. Every viewport's prompt of that runbook runs against that one log. The agent prints the next runbook's prompts only after the --import of the previous one: the import step in instructions/run.md (step 6) reads the log, then selects the next runbook and prints its prompts. With role tracking on, the unit is a runbook and a role: the agent selects { runbook, mode, role }, prints that role's prompts (one per viewport) and names the roles and runbooks still waiting. After the --import of that role's reply, it reads the log, saves the reply, selects the next role ({ role }) and prints its prompts the same way. After the last role's import, it writes the runbook's one report from the saved replies, then selects the next runbook. With cases, the unit is a runbook, a role and a case, because the server serves one case at a time: the agent selects { runbook, mode, role, case } with the pass's dataCase, prints the prompts of that pass (one per viewport), and after their import selects the next pass in the order of passes <id>. It sends { case } for the next case of the same role, and { role, case } for the next role, because a select that names a role without a case resets the case. The report's Notes say that step folders of fixtures are not served in a prompt run. The server keeps running until the import of the last runbook, which stops it. When it no longer runs at import, the states come from the reply alone and the Notes say unmatched was not checked.

Import

The reply block:

RUNBOOK RESULT <runbook id> as <role> in <case> @ <viewport>
S01 | passed | <what was seen>
S02 | failed | <what was seen>
END
  • The id after RUNBOOK RESULT must match the runbook. as <role> names one of its roles, and is absent when role tracking is off. in <case> names the case of the pass, and is absent for a runbook with no context and for a role that runs once. @ <viewport> names one of its viewports, and is absent when the runbook has none.
  • Every step that runs for that role, in that case and at that viewport appears once, with one of the step states.
  • With viewports, the report waits for a reply from every viewport. With role tracking on, it waits for every role too, and for every case of each role with cases: each import saves its replies (instructions/run.md, step 6), and the question about a missing reply comes only after the last import. When one is missing, ask whether to import it later or record its steps as blocked.
  • Ask the user about missing, repeated or unknown lines before writing anything.
  • The report's executor is "Claude in Chrome extension, result reported by the user" and mode is prompt.

View source on GitHub