> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.doc-detective.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.doc-detective.com/_mcp/server.

# record

> Start capturing a video of test execution.

The `record` action starts recording test execution as a video file (MP4, WebM, or GIF). This is useful for debugging failed tests or creating multimedia documentation. Recordings are video only, with no audio track. Recording continues until your test reaches a `stopRecord` step.

> **Note**
>
> Recording works with concurrent test runners (`concurrentRunners` > 1). The `browser` engine records each context's own window, so it always runs concurrently. The `ffmpeg` engine captures the whole shared display, so Doc Detective serializes `ffmpeg` recordings with other browser-based tests automatically. You don't need to set `concurrentRunners` to `1`. See [Recording engines](#recording-engines) for details.

You can specify the recording action in several ways:

* **Boolean Shorthand:** Set `record: true` to start recording with default settings (usually saved as `record_<timestamp>.mp4` in the output directory).
* **String Shorthand:** Provide a file path directly as the value for the `record` key (for example, `record: "test_session.webm"`). This starts recording to the specified path. Supported extensions are `.mp4`, `.webm`, and `.gif`.
* **Object Format:** Use an object for more control over the output file:
  * `path`: (Optional) The file path where the recording will be saved. Can include directories. If omitted, a default path is generated.
  * `directory`: (Optional) The directory where the recording will be saved. If `path` includes a directory, this is ignored. Defaults to the configured output directory.
  * `overwrite`: (Optional) Set to the string `"true"` to overwrite an existing file at the target path. Defaults to `"false"`.
  * `name`: (Optional) An identifier for this recording. A later `stopRecord` step can target it by name to stop a specific recording when several overlap. See [Multiple overlapping recordings](#multiple-overlapping-recordings) below.
  * `engine`: (Optional) The recording engine to use. See [Recording engines](#recording-engines) below.
  * `surface`: (Optional) The surface to record. For browsers, a session, window, or tab reference, for example `{ "browser": "chrome", "tab": "demo" }`; omit to record the active tab. The recorder's own internal tab is never addressable by window/tab selectors. See [Test across multiple tabs, windows, and browsers](/docs/test-docs/multiple-tabs-and-windows). For native apps, the object form `{ "app": "<surface name>" }` records that app, cropped to its window by default. See [Recording app surfaces](#recording-app-surfaces).

**Stopping the Recording:** Include a `stopRecord` step to end a recording at a specific point in your test. Doc Detective automatically finalizes any remaining recordings at the end of the test run.

## Recording engines

Recording uses one of two engines, selected by the optional `engine` field in the object format:

* **`browser`:** Records the Chrome viewport using the browser's screen-capture API. It captures only the current browser tab. It's safe to run with concurrency (when `concurrentRunners` is greater than 1), because each context records its own window. The `browser` engine requires a visible Chrome context and can't record in headless mode.
* **`ffmpeg`:** Records the screen with FFmpeg, so it works for any application, not just Chrome. Doc Detective installs FFmpeg automatically, so you don't need to install it yourself.

If you don't set `engine`, Doc Detective chooses one for you. It prefers the `browser` engine when a visible Chrome context is available (coercing an unspecified browser to headed Chrome), and falls back to `ffmpeg` otherwise.

Set `engine` as a string to select an engine with its defaults (for example, `"engine": "ffmpeg"`), or as an object for more control:

* `name`: (Required) The engine to use, either `browser` or `ffmpeg`.
* `target`: (Optional, `ffmpeg` only) What the engine captures. Use `display` to record the full screen, `window` to record the active window, or `viewport` to record the browser content area. If unset, the target defaults to `window` when the recording's `surface` is an app surface and to `display` otherwise. The `window` and `viewport` targets are best-effort: FFmpeg captures the full screen and then crops the result. The `browser` engine ignores this field and always captures its tab.
* `fps`: (Optional, `ffmpeg` only) The capture frame rate (default: `30`).

On Android and iOS contexts, `engine` doesn't apply. Doc Detective records the device screen through the device itself. See [Recording on mobile devices](#recording-on-mobile-devices).

**Recording concurrency** depends on the engine. The `browser` engine is always safe to run concurrently, because each context records its own window.

The `ffmpeg` engine captures the whole shared display. Two `ffmpeg` recordings would therefore overwrite each other's output, as would an `ffmpeg` recording running alongside another browser-driven test. To prevent this, Doc Detective serializes that recording with every other browser-based test so they take turns on the display. Jobs that don't drive a browser, such as `httpRequest` and `runShell` steps, keep running in parallel up to `concurrentRunners`. This behavior applies on every platform, including Linux. To run the recordings concurrently as well, use the `browser` engine instead.

On Linux, the `ffmpeg` engine also needs Xvfb to record headless contexts, which have no physical display to capture. Install Xvfb to record in headless mode.

## Recording app surfaces

A `record` step can target a native app surface opened by [`startSurface`](/docs/actions/startsurface) with the object form `{ "app": "<surface name>" }`:

```json
{
  "record": {
    "path": "notepad-demo.mp4",
    "surface": { "app": "notepad" }
  }
}
```

App-surface recordings use the `ffmpeg` engine with a `window` target by default. FFmpeg captures the full display, and when the recording stops, Doc Detective crops the video to the app's window. That includes scaled (HiDPI/Retina) displays, where Doc Detective scales the crop to match the capture's physical pixels. Set `engine: { "name": "ffmpeg", "target": "display" }` to keep the full display instead. The `viewport` target is the browser content area, so it doesn't apply to app surfaces. Doc Detective skips a record step that combines them and explains why. It does the same for the `browser` engine, which records a Chrome tab rather than a native window.

To record a specific window of the app, such as a dialog or one of several document windows, add a `window` selector: `{ "app": "notepad", "window": { "title": "/Open/" } }`. The crop then bounds that window. See [App windows](find#app-windows) for the selector grammar.

App-surface recordings join the same display serialization as other FFmpeg recordings, and the same overlapping/named-stop rules apply.

Native macOS and Windows app contexts add a second layer of serialization. Each host has a single app driver, and two native app contexts can't run against it at once without overwriting each other's session. So Doc Detective serializes native app contexts on that shared driver. A recording app context takes its turn alongside every other native app context on the host, whether or not that other context records. This keeps recording a native app surface safe under `concurrentRunners` greater than 1. The recording context queues for the driver instead of colliding with a concurrent app context. Contexts that don't drive a native app driver or the display, such as `httpRequest` and `runShell` steps, keep running in parallel.

> **Note**
>
> Desktop screen capture records whatever is visible in the captured region. If another window overlaps the app during the recording, it appears in the video. Doc Detective doesn't hide or minimize other windows. On a dedicated CI runner this rarely matters. On a shared desktop, keep the recorded app in the foreground.

## Recording on mobile devices

On Android and iOS contexts, recordings capture the **device screen** through the device itself, without touching the host display. Android uses its screen recorder, and iOS uses the simulator's. This means:

* Omit `engine`. It doesn't apply, and Doc Detective skips an explicit `browser` or `ffmpeg` engine on a mobile context with guidance.
* Mobile recordings run under full concurrency. They never serialize with FFmpeg recordings or other browser tests, because they don't touch the shared display.
* The recording is the whole device screen; there's no window cropping to configure.
* All output formats work: Doc Detective saves `.mp4` directly and converts `.webm` and `.gif` after the recording stops.
* One recording can run per device at a time, and a single recording caps at 30 minutes.

```json
{
  "record": {
    "path": "android-flow.mp4",
    "surface": { "app": "settings" }
  }
}
```

A `record` step can run before the first `startSurface` on a mobile context. It waits for the device session, then starts recording as soon as the first app surface opens.

## Multiple overlapping recordings

A single context can run more than one recording at once. For example, you can capture a full test alongside a shorter clip of one workflow inside it. Give each `record` step a `name` so a later `stopRecord` step can target the right one:

* An untargeted `stopRecord` (`true` or `null`) stops the most recently started recording that's still active (last in, first out).
* To stop a specific recording, target it by `name`, either as a string (`stopRecord: "<name>"`) or an object (`stopRecord: { name: "<name>" }`). This works for any active recording, not just the most recent one.

Names must be unique among recordings that are active at the same time. Suppose a `record` step reuses the `name` of a recording that's still active. Doc Detective catches the conflict before the run starts and skips that test in every context with a warning, rather than failing partway through.

The `browser` engine can own only one recorder per context. So when two recordings overlap and both would use it, the second falls back to the `ffmpeg` engine with a `viewport` target and emits a warning. See [Recording engines](#recording-engines) for details on engine selection.

## Automatic recordings

Enable the `autoRecord` option to record every driver-based test context from start to finish, covering browser, native app, and mobile contexts alike. It captures each context end-to-end without explicit `record` steps, which is useful for:

* Building a video archive of every run
* Replaying exactly what happened when a test fails
* Comparing behavior from one run to the next

Enable it at the config, spec, or test level (test overrides spec, which overrides config):

```json
{
  "autoRecord": true,
  "output": "./test-results"
}
```

Or use the CLI flag:

```bash
doc-detective --auto-record
```

Use `--no-auto-record` to override a config file that enables it.

Automatic recordings wrap the whole context. Recording starts before the first step and stops after the last. Desktop contexts capture with the `ffmpeg` engine. In an app-only context the capture starts full-display, and the first app surface to open scopes it to that app's window. Android and iOS contexts record the device screen instead, starting as soon as the first app surface opens its device session. Doc Detective saves each video in the per-run artifact folder (`<output>/.doc-detective/runs/<runId>/`) under the nested resource tree. The same context therefore lands on the same relative path in every run's folder:

```
.doc-detective/runs/<runId>/
└── specs/
    └── <specId>/
        └── tests/
            └── <testId>/
                └── contexts/
                    └── <contextId>/
                        └── recordings/
                            └── <contextId>.mp4
```

An automatic recording coexists with any explicit `record` and `stopRecord` steps in the same context. An untargeted `stopRecord` never stops the automatic recording, so a stray `stopRecord: true` can't cut the full-context video short. Doc Detective finalizes it during end-of-context cleanup.

> **Note**
>
> On Linux, each runner records its own virtual display with Xvfb, so automatic recordings stay isolated even when runs are concurrent. On Windows and macOS, concurrent automatic recordings share the same physical display and overlap. Doc Detective warns when a run records concurrently on those platforms.

## Recording checkpoints

A recording documents a flow, but the steps can keep passing while the recorded content quietly drifts. A page gets redesigned, a button moves, copy changes. To detect that drift, enable `checkpoints` on a `record` step:

```json
{
  "record": {
    "path": "signup-flow.mp4",
    "checkpoints": true
  }
}
```

While the recording is active, Doc Detective captures a screenshot after every step, and the `record` step's own capture is the opening bookend. It compares each against a persistent baseline stored beside the recording, `signup-flow.mp4.checkpoints/` by default. On the first run, the baselines don't exist yet, so `stopRecord` seeds them from the captures. Commit the baseline folder alongside your recording. On every later run, each checkpoint compares against its baseline:

* Variation within `maxVariation` (default `0.05`): the step passes.
* Variation beyond `maxVariation`: the `stopRecord` step returns WARNING. The recorded flow's content has probably changed since the recording was captured. A checkpoint comparison never modifies baselines.

Checkpoints keep comparing even when the recording itself doesn't re-record. By default (`overwrite: "false"`), Doc Detective skips a `record` step whose file already exists. Once you commit the recording, that means every run after the first. Doc Detective skips the video but still runs the checkpoints against the committed baselines and reports drift. You keep the signal without rewriting the file. See [Detecting staleness without re-recording](#detecting-staleness-without-re-recording).

The `stopRecord` step reports per-checkpoint results in its [outputs](stoprecord): `checkpoints` (file name and variation per checkpoint), `maxCheckpointVariation`, `seededBaselines`, and `checkpointErrors`.

Tune or relocate the comparison with the object form:

```json
{
  "record": {
    "path": "signup-flow.mp4",
    "checkpoints": {
      "maxVariation": 0.1,
      "directory": "baselines/signup-flow"
    }
  }
}
```

A relative `directory` resolves beside the recording. Raise `maxVariation` (or disable checkpoints) for spans that show dynamic content such as animations, ads, and clocks. Those otherwise produce WARNING noise. Checkpoints apply only to explicit `record` steps, because [automatic recordings](#automatic-recordings) save to per-run folders where baselines couldn't persist. Each checkpoint costs one extra screenshot per step while the span is active.

### Refreshing stale recordings automatically

Checkpoints detect drift, and `overwrite: "aboveVariation"` acts on it. With this setting, the span always records, but the new video replaces the existing file **only when the span meaningfully changed**. That means checkpoint drift beyond `maxVariation`, an added, removed, or edited step, or a missing recording file. Otherwise Doc Detective discards the fresh capture and leaves the recording and its baselines byte-untouched. Unchanged recordings stay stable across runs:

```json
{
  "record": {
    "path": "signup-flow.mp4",
    "overwrite": "aboveVariation"
  }
}
```

`aboveVariation` requires checkpoints, so it enables them automatically. It overrides an explicit `checkpoints: false`, because a verdict with no evidence could never refresh a drifted recording. Add a `checkpoints` object to tune them. When the recording refreshes, its baselines update with it in the same step, so they describe the video you have. If a run is interrupted partway through that update, the next run sees the mismatch as drift and refreshes both again. It repairs itself rather than leaving you to notice. The `stopRecord` step reports `changed: true` with the reasons in its outputs. If a step fails during the span, Doc Detective keeps the existing recording and baselines untouched rather than judging drift from an incomplete run.

### Verifying the video file itself

Checkpoints validate the scene a recording shows, while `verify` validates the produced file. Use it to assert structural properties of the video. The classic capture failure modes are an all-black video, a wrong-sized crop, and a truncated clip:

```json
{
  "record": {
    "path": "signup-flow.mp4",
    "verify": {
      "minDuration": 2,
      "resolution": true,
      "notBlack": true
    }
  }
}
```

Unlike checkpoint drift (a WARNING), a violated guard **fails** the `stopRecord` step. These are properties you explicitly demand of the artifact:

* `minDuration` / `maxDuration`: the video's length in seconds. Allow about half a second of slop for `.gif` targets, whose duration reporting is imprecise.
* `resolution`: the video's dimensions, with a ±2 pixel tolerance (encoders round to even dimensions). `true` compares against the capture plan's expectation. That's the crop rectangle for `window` and `viewport` targets, and otherwise the capture frame size. The check is skipped when the engine reports no expectation. Pass `{ "width": …, "height": … }` to compare literal dimensions.
* `notBlack`: fails when black frames cover 95% or more of the video, the symptom of a broken screen capture.

The measured values land in the [`stopRecord` outputs](stoprecord) (`$$duration`, `$$width`, `$$height`, plus `$$resolutionMatch` and `$$allBlack` when the matching guard ran).

### Detecting staleness without re-recording

Doc Detective skips the capture in two situations. The first is a headless run, since recording needs a visible display. The second is a target file that already exists under the default `overwrite: "false"`. In both, a step with `checkpoints` (or `aboveVariation`, which implies them) still compares its checkpoints against the committed baselines and reports whether the recording has gone stale. The `stopRecord` step returns WARNING with `stale: true` in its outputs, and a post-run hint points you at the fix.

The fix depends on which skip you hit, and the `stopRecord` step's description names it:

* **Headless run:** re-run the test on a headed context to refresh the recording.
* **Target already exists:** set `overwrite` to `"true"` or `"aboveVariation"`, or remove the file. With `aboveVariation`, the recording refreshes itself the moment it drifts, so this never comes up.

Either way, detection is cheap and continuous. Your headless CI watches recording freshness on every run, and you only re-record when something actually changed. Skipped spans never write. There's no video, no baseline seeding, and no baseline updates.

> For comprehensive options, see the [`record`](/reference/schemas/record) and [`stopRecord`](/reference/schemas/stoprecord) references.

## Examples

Here are a few ways you might use the `record` and `stopRecord` actions:

### Basic recording (boolean shorthand)

Starts recording to a default path like `output/record_1745032062266.mp4` and stops later.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Start recording.",
          "record": true
        },
        {
          "description": "Perform some actions...",
          "goTo": "https://example.com"
        },
        {
          "description": "Stop recording.",
          "stopRecord": true
        }
      ]
    }
  ]
}
```

### Recording to a specific path (string shorthand)

Records to `./output/media/login_flow.webm`.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Start recording to a specific WebM file.",
          "record": "./output/media/login_flow.webm"
        },
        {
          "description": "...",
          "find": "Login"
        },
        {
          "description": "Stop recording.",
          "stopRecord": true
        }
      ]
    }
  ]
}
```

### Recording with object format (overwrite enabled)

Records to `test_video.gif` in the `output/gifs` directory, overwriting if it exists.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Start recording a GIF, overwriting if necessary.",
          "record": {
            "path": "test_video.gif",
            "directory": "./output/gifs",
            "overwrite": "true"
          }
        },
        {
          "description": "...",
          "wait": 2000
        },
        {
          "description": "Stop recording.",
          "stopRecord": true
        }
      ]
    }
  ]
}
```

### Pinning the browser engine

Records the Chrome tab with the `browser` engine instead of letting Doc Detective pick one, so the recording is safe to run with concurrency.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Start recording the Chrome tab with the browser engine.",
          "record": {
            "path": "browser_demo.mp4",
            "engine": "browser"
          }
        },
        {
          "description": "...",
          "goTo": "https://example.com"
        },
        {
          "description": "Stop recording.",
          "stopRecord": true
        }
      ]
    }
  ]
}
```

### Recording any application with the FFmpeg engine

Records the screen with FFmpeg so the recording isn't limited to Chrome.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Start recording the screen with FFmpeg.",
          "record": {
            "path": "demo.mp4",
            "engine": "ffmpeg"
          }
        },
        {
          "description": "...",
          "runShell": "my-cli --help"
        },
        {
          "description": "Stop recording.",
          "stopRecord": true
        }
      ]
    }
  ]
}
```

### Overlapping recordings targeted by name

Captures a full session while also recording a shorter clip of one step, stopping each by name.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Start recording the full session.",
          "record": {
            "path": "session.mp4",
            "name": "session"
          }
        },
        {
          "description": "Start a clip of the login step.",
          "record": {
            "path": "login.mp4",
            "name": "login"
          }
        },
        {
          "description": "...",
          "find": "Login"
        },
        {
          "description": "Stop the login clip.",
          "stopRecord": "login"
        },
        {
          "description": "...",
          "goTo": "https://example.com"
        },
        {
          "description": "Stop the full session.",
          "stopRecord": { "name": "session" }
        }
      ]
    }
  ]
}
```

### Recording a native app window

Records the app's window (the default crop for app surfaces). On an Android or iOS context, the same step shape records the device screen instead.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Launch the app.",
          "startSurface": { "app": "C:\\Windows\\System32\\notepad.exe" }
        },
        {
          "description": "Record the app window.",
          "record": {
            "path": "notepad-demo.mp4",
            "surface": { "app": "notepad" }
          }
        },
        {
          "description": "...",
          "type": { "keys": ["Hello!"], "surface": { "app": "notepad" } }
        },
        {
          "description": "Stop recording.",
          "stopRecord": true
        }
      ]
    }
  ]
}
```

### Configuring the FFmpeg engine (detailed object)

Records the active window at 60 fps with FFmpeg.

```json
{
  "tests": [
    {
      "steps": [
        {
          "description": "Start recording the active window.",
          "record": {
            "path": "demo.mp4",
            "engine": {
              "name": "ffmpeg",
              "target": "window",
              "fps": 60
            }
          }
        },
        {
          "description": "...",
          "wait": 2000
        },
        {
          "description": "Stop recording.",
          "stopRecord": true
        }
      ]
    }
  ]
}
```