Skip to content

Capture documentation screenshots

Capture application screens with docs:screenshot, or render a FlowScript workflow with workflow:screenshot. Both commands use the desktop frontend and write images suitable for the documentation site or FlowBook.

Install the repository toolchain and dependencies first. The screenshot runner uses Puppeteer and its Chromium browser; workflow captures also compile a Rust helper. Run commands from the repository root. See documentation assets for publication conventions.

The checked-in plan opens onboarding with capture=docs, captures the initial profile grid, selects the fourth profile, and captures both the selected and completed states:

Terminal window
bun run docs:screenshot -- \
--plan apps/desktop/lib/doc-screenshot/examples/onboarding.plan.json \
--json

The three lossless WebP files are written below tmp/doc-screenshots/onboarding.

Plan paths use these bases:

  • tauriFixture and httpFixture are resolved relative to the plan file. This keeps a plan and its fixtures portable when the command is launched from another directory.
  • outputDir is resolved relative to the process working directory. From the repository root, the example therefore writes to tmp/doc-screenshots/onboarding.

Refresh the checked-in documentation screenshots

Section titled “Refresh the checked-in documentation screenshots”

The documentation plans write directly to apps/docs/src/assets and use dark mode, a 1624 by 1060 CSS-pixel viewport, DPR 2, and lossless WebP:

Terminal window
bun apps/desktop/scripts/generate-doc-screenshot-fixtures.ts
bun apps/desktop/scripts/generate-doc-studio-screenshot-fixture.ts
bun run docs:screenshot -- --plan apps/desktop/lib/doc-screenshot/examples/docs-start.plan.json
bun run docs:screenshot -- --plan apps/desktop/lib/doc-screenshot/examples/docs-apps.plan.json
bun run docs:screenshot -- --plan apps/desktop/lib/doc-screenshot/examples/docs-ontology.plan.json
bun run docs:screenshot -- --plan apps/desktop/lib/doc-screenshot/examples/docs-sharing.plan.json
bun run docs:screenshot -- --plan apps/desktop/lib/doc-screenshot/examples/docs-roles.plan.json
bun run docs:screenshot -- --plan apps/desktop/lib/doc-screenshot/examples/docs-studio.plan.json
bun run docs:screenshot -- --plan apps/desktop/lib/doc-screenshot/examples/docs-reference.plan.json
bun run docs:screenshot -- --plan apps/desktop/lib/doc-screenshot/examples/docs-developer-mode.plan.json

Each plan starts from application routes and performs the navigation and UI interactions needed to expose the documented state. A failed wait or action fails that scenario instead of silently writing a loading or error screen.

Use direct mode for one route and one capture:

Terminal window
bun run docs:screenshot -- \
--app web \
--path /onboarding \
--query capture=docs \
--output tmp/doc-screenshots/onboarding/direct-web.webp \
--viewport 1624x1060 \
--dpr 2 \
--theme light \
--wait-for h1 \
--json

--query accepts a key=value pair and can be repeated. Use --full-page for a full document capture, or --selector <css-selector> to capture one element. The output extension selects PNG, WebP, or JPEG.

The CLI starts the selected Next app automatically. Use --frontend-url http://127.0.0.1:PORT to reuse a running loopback server, --port <number> to change the automatically started server’s port, or --keep-server to leave a server started by the CLI running.

Plans use the flow-like.doc-screenshot-plan/v1 schema. A plan declares its app, output directory, optional desktop Tauri fixture, optional browser HTTP fixture, render defaults, and one or more scenarios. Each scenario starts at path plus an optional query object and runs its steps in order.

The supported steps are:

Step Fields Behavior
waitFor one of selector, urlIncludes, or text; optional state, timeoutMs Waits for a DOM, URL, or text condition. Selector states are attached, visible, hidden, and detached.
click selector, optional index, button, clickCount, modifiers Clicks the matching element. index is zero-based. modifiers accepts a unique subset of Alt, Control, Meta, and Shift.
drag selector, targetSelector, optional index, targetIndex, steps, button, release Drags between the centers of two matching elements. Both centers must be visible and unobscured. steps is 1–100 (default 20). release defaults to true; set it to false only when the next step captures the held-pointer state. The button is released after that capture.
fill selector, value or valueEnv, optional index Replaces the value of an input.
type selector, value or valueEnv, optional index, delayMs Types into an input.
press key, optional selector, index Sends a keyboard key globally or to an element.
select selector, values, optional index Selects one or more values in a native select element.
check selector, optional index, checked Sets a checkbox or radio control’s checked state.
hover selector, optional index Moves the pointer over an element.
scroll optional selector, index, x, y Scrolls the page or a matching element.
goto path, optional query Navigates to another same-app route and waits for it to settle.
delay ms Waits for an explicitly bounded interval. Prefer a semantic waitFor when possible.
capture name, optional mode, selector, index, padding, output, format, quality, hideSelectors Writes a named viewport, fullPage, or element screenshot. Element mode requires a selector and scrolls the target into view before measuring it.

One complete working example is in examples/onboarding.plan.json. Prefer a plan when documentation needs multiple states: it is easier to review and rerun than a sequence of shell commands.

The onboarding example uses a 1624 by 1060 CSS-pixel viewport at device scale factor 2, producing a 3248 by 2120 pixel viewport image. It also fixes the theme to light, disables CSS animations and transitions, hides scrollbars, allows 250 ms of settling after actions, and gives cold desktop hydration up to 120 seconds.

PNG and WebP output are encoded losslessly. JPEG alone uses the optional numeric quality setting. The tool waits for the requested selector and the page render boundary before capture; it does not upscale screenshots after capture. Keep fonts, thumbnails, and icons local when repeatability matters.

Browser Chromium does not have a native Tauri runtime. A desktop plan can provide a JSON fixture which installs a deterministic IPC mock before any app code runs. The onboarding fixture uses only checked-in /swimlanes/*.jpg thumbnails and /flow/icons/*.svg bit icons, so profile cards do not depend on remote media.

Fixtures use this shape:

{
"schema": "flow-like.doc-screenshot-tauri-fixture/v1",
"strict": true,
"responses": {
"get_profiles": {},
"get_bit_size": 6291456
}
}

responses is keyed by the exact Tauri command name. Every call to that command receives the same JSON response, independent of its arguments. Values must therefore be immutable fixture data, not stateful behavior. With strict: true, an unlisted command fails the scenario. With strict: false, an unlisted command resolves to null; list important calls explicitly even when their response is only a no-op.

A response may model a bounded asynchronous command and emit Tauri events while it is pending. This is useful for real progress UI such as model downloads:

{
"$value": [],
"$delayMs": 60000,
"$events": [
{
"afterMs": 500,
"name": "download:model-id",
"payload": {
"downloaded": 671088640,
"max": 2147483648
}
}
]
}

Event delays and the command delay are capped at 120 seconds, and at most 100 events are scheduled for one invocation.

Tauri HTTP uses two IPC commands. plugin:http|fetch returns a request resource ID, and plugin:http|fetch_send returns response metadata such as status, headers, URL, and a response resource ID. A 204 No Content response avoids a body-read command and is useful for deterministic background requests.

See fixtures/onboarding.tauri.json for realistic profile, bit, download, event, updater, notification, registry, tray, and HTTP responses.

A plan can set httpFixture to serve deterministic browser responses without starting an API. This is separate from Tauri IPC HTTP mocking and works for normal fetch, XHR, images, and other Chromium requests.

Fixtures use exact request matches:

{
"schema": "flow-like.doc-screenshot-http-fixture/v1",
"strict": true,
"blockedOrigins": [
"https://telemetry.example.test"
],
"blockedEndpoints": [
"http://localhost:8080/api/v1/og"
],
"routes": [
{
"request": {
"method": "GET",
"url": "http://localhost:8080/api/v1/auth/openid"
},
"response": {
"status": 200,
"headers": {
"access-control-allow-origin": "*"
},
"json": {
"authority": "http://localhost:8080",
"client_id": "flow-like-doc-screenshot"
}
}
}
]
}

A match compares the uppercase HTTP method, canonical absolute URL (including query order), and, when declared, the raw request body. Omitting request.body accepts any body for that exact method and URL, which is useful for non-deterministic telemetry envelopes that should be absorbed rather than sent. There are no URL wildcard or regular-expression matches. A response can contain either a raw string body or a JSON-serializable json value; JSON responses receive an application/json content type unless the fixture declares one.

Same-origin frontend requests always continue so Next.js pages, chunks, and local assets can load. blockedOrigins lists exact HTTP origins whose requests are intentionally aborted without failing the scenario; use it for product telemetry that must never leave a documentation capture. blockedEndpoints does the same for one exact origin and path while ignoring its query string, which is useful for non-essential preview endpoints with dynamic URL parameters. With strict: true, any other unmatched cross-origin HTTP request is blocked and fails the scenario with its method and redacted URL. strict: false lets unmatched cross-origin requests use the network and should be reserved for exploratory captures. Cross-origin requests that trigger CORS preflight need an exact OPTIONS route as well as the application request.

The reference plan uses fixtures/docs-reference.http.json to provide the OpenID configuration required by /debug/markdown. The route is public, including when opened without the plan’s capture=docs marker, but the app’s OpenID fetch remains mandatory: a missing, mismatched, or invalid fixture response still fails instead of falling back to an unauthenticated render.

Pass --json for a flow-like.doc-screenshot-result/v1 result for scripts or CI. It reports scenario and step status, final URL, output files, dimensions, byte counts, SHA-256 hashes, timings, and bounded page error counts. Exit code 0 means every scenario passed, 1 means a scenario, action, or capture failed, and 2 means the CLI, server, browser, or input contract failed.

All input formats are versioned and validated before the browser starts:

  • Plans: flow-like.doc-screenshot-plan/v1
  • Tauri fixtures: flow-like.doc-screenshot-tauri-fixture/v1
  • Browser HTTP fixtures: flow-like.doc-screenshot-http-fixture/v1

Plans are declarative by design. They cannot execute JavaScript or shell commands. Navigation is limited to application routes, selectors and waits are timeout-bounded, output names cannot escape the configured output directory, and fixture files contain JSON only.

Do not place passwords, access tokens, private headers, or other secrets in a plan, query string, fixture, selector, or filename. These inputs can appear in logs and result metadata, and the rendered page itself becomes part of the screenshot. Use synthetic fixture data for documentation captures.

workflow:screenshot turns a catalog-valid FlowScript document into a rendered Studio workflow. Use it for FlowBook illustrations and workflow reference images.

Run it from the repository root:

Terminal window
bun run workflow:screenshot -- \
apps/book/examples/incident-triage/triage.flow \
--output apps/book/src/assets/workflows/incident-triage.webp \
--layout balanced \
--theme light

The pipeline uses the production pieces in their normal order:

  1. The Rust helper applies the source to an empty Board through apply_flowscript_to_board and the complete built-in catalog. Parse or reconcile diagnostics stop the command before a browser starts.
  2. The resulting Board is formatted with the same computeFlowLayoutDetailed engine used by Studio. Root and nested/function-layer canvases are all laid out.
  3. An ephemeral offline app and Board are exposed to the desktop frontend through the documentation Tauri fixture bridge. No real profile, app, or Board is created or changed.
  4. The existing documentation screenshot runner opens /flow in Chromium and writes a lossless WebP/PNG (or JPEG) at the requested viewport and DPR.

--focus-node accepts an exact reconciled ID, a node/layer identity anchor such as //@n:abc123 or //@l:function123, a unique catalog node name, a friendly name, or a layer name. The normal /flow?...&node=<id> navigation opens the owning layer and frames the target with Studio’s focus behavior.

Generated ids are easiest to discover with:

Terminal window
bun run workflow:screenshot -- path/to/workflow.flow --list-nodes

Then render the detail:

Terminal window
bun run workflow:screenshot -- path/to/workflow.flow \
--focus-node normalize \
--output tmp/workflow-screenshots/normalize.webp

An ambiguous selector fails and prints the matching ids instead of silently choosing one. When a document contains only function declarations, the renderer automatically opens the first function by stable name/id order so the root’s intentionally hidden function layers cannot produce an empty capture.

--handle-errors adds the same On Error Execution output and Error String output as Studio’s Handle Errors toggle. It accepts the same node ids, node anchors, catalog names, and friendly names as --focus-node; layers and pure nodes are rejected. The adjusted node is focused automatically unless --focus-node explicitly selects another target.

Terminal window
bun run workflow:screenshot -- path/to/workflow.flow \
--handle-errors "API Call" \
--output tmp/workflow-screenshots/api-error.webp

The outputs are added only to the ephemeral reconciled Board used for rendering. The FlowScript source and any real Flow-Like profile remain unchanged.

  • --layout compact|balanced|expanded selects Studio’s layout style. balanced is the default for book-friendly spacing.
  • --viewport 1624x1060, --dpr 2, and --theme light|dark control the deterministic browser surface.
  • .webp and .png are lossless. .jpg/.jpeg can use --quality.
  • --frontend-url http://127.0.0.1:3000 reuses an already running desktop frontend.
  • --json returns the screenshot hash, dimensions, resolved focus id, and nested capture result on stdout. Progress and server logs stay on stderr.

The default output is tmp/workflow-screenshots/<input-name>.webp.

For repeated captures after building the helper once, set FLOW_LIKE_FLOWSCRIPT_RENDER_DATA_BIN=target/debug/flowscript-render-data to bypass Cargo’s workspace lock and invoke that exact binary directly.