> 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.

# annotate

> Draw annotations into the page so they stay on screen and appear in recordings.

The `annotate` action draws annotations into the page and leaves them there. They stay up across steps, survive navigation, and appear in any recording or screenshot taken while they're visible.

Here's how `annotate` differs from the [`annotations` option on a screenshot](/docs/actions/screenshot#annotations):

|                       | `screenshot.annotations` | `annotate`                     |
| --------------------- | ------------------------ | ------------------------------ |
| Lifetime              | One capture              | Until something clears them    |
| Appears in recordings | No                       | Yes                            |
| Works on app surfaces | Yes                      | No. Needs a page to draw into. |
| Touches the page      | No                       | Yes                            |

Use `screenshot.annotations` to mark up a single image. Use `annotate` to narrate a video, or to keep a redaction up across a whole flow.

Both accept the same annotation objects, so `{ "outline": "#submit" }` means the same thing in either.

## Options

Give the object at least one of:

* `add`: Annotations to draw. Give one an `id` to update or clear it later. Adding an annotation with an id that's already on screen replaces it.
* `update`: Changes to annotations already on screen, matched by `id`. Each entry replaces the annotation with that id. Updating an id that isn't on screen fails the step, since it usually means a typo or an already-cleared annotation.
* `clear`: `true` clears everything; a list of ids clears just those. Clearing an id that isn't on screen does nothing.

`clear` runs before `add`, so `{ "clear": ["a"], "add": [{ "id": "a", … }] }` reads as replacing `a`.

The [screenshot annotations](/docs/actions/screenshot#annotations) page covers the annotation objects themselves: the six types, targets, `style`, and the `annotationDefaults` theme. Four fields matter here that do nothing in a still screenshot:

* `id`: A handle, so a later step can update or clear this annotation.
* `track`: Follow the element as the page scrolls or the layout shifts.
* `transition`: How the annotation enters and leaves. `enter` accepts `none`, `fade`, `pop`, or `draw`. `exit` accepts `none` or `fade`. `durationMs` sets the length.
* `duration`: Milliseconds to stay up before clearing itself, with no paired `clear` step.

> **Warning**
>
> Set `"transition": { "enter": "none" }` on a `blur` you're using to redact. An annotation that fades in leaves the content underneath readable for the length of the animation, and every frame of that is in the recording.

### Annotate something that hasn't loaded yet

An element target waits up to 5000 milliseconds for its element to appear, then fails the step. Set `timeout` on the target when you're annotating something slower. Examples include a panel behind a fetch, or a dashboard that renders after a first sync:

```json
{
  "annotate": {
    "add": [
      {
        "id": "guide",
        "callout": { "elementTestId": "dashboard", "timeout": 20000 },
        "label": "Your data lands here after the first sync"
      }
    ]
  }
}
```

This is [`find`](/docs/actions/find)'s `timeout`, on the annotation's target. Two things follow from that. It needs the object form of the target, since the string shorthand has nowhere to put it. It's also per-target, so one slow element in a payload doesn't make the others wait.

`update` re-resolves its target, so give it a `timeout` too when you point an annotation at something that isn't on the page yet.

**Outputs:** the action exposes `$$annotationCount`, the number of annotations on screen after the step.

> For comprehensive options, see the [`annotate`](/reference/schemas/annotate) reference.

## Examples

### Narrate a recording

Annotations track the flow, moving from field to field as the walkthrough progresses.

```json
{
  "tests": [
    {
      "steps": [
        { "record": "sso-setup.webm" },
        {
          "description": "Point at the first field.",
          "annotate": {
            "add": [
              { "id": "step", "badge": "#idp", "label": "1" },
              {
                "id": "guide",
                "callout": "#idp",
                "label": "Pick your provider first",
                "position": "right",
                "track": true
              }
            ]
          }
        },
        { "click": "#idp" },
        {
          "description": "Move the same annotations to the next field.",
          "annotate": {
            "update": [
              { "id": "step", "badge": "#metadata-url", "label": "2" },
              { "id": "guide", "callout": "#metadata-url", "label": "Paste the metadata URL" }
            ]
          }
        },
        { "annotate": { "clear": true } },
        { "stopRecord": true }
      ]
    }
  ]
}
```

### Redact for a whole recording

The blur stays glued to every match while the page scrolls, and never animates in.

```json
{
  "annotate": {
    "add": [
      {
        "id": "redact",
        "blur": { "selector": ".account-id" },
        "all": true,
        "track": true,
        "transition": { "enter": "none" }
      }
    ]
  }
}
```

### Show a note briefly

`duration` clears the annotation on its own.

```json
{
  "annotate": {
    "add": [
      {
        "outline": "#usage-meter",
        "duration": 3500,
        "transition": { "enter": "draw", "exit": "fade", "durationMs": 400 }
      }
    ]
  }
}
```

## How it works

Doc Detective draws annotations into a `dd-annotation-layer` element with its own shadow root, sitting above the page and ignoring pointer events. The page under test keeps behaving exactly as it would without annotations, and the page's CSS can't restyle them.

Doc Detective tracks the annotations itself, so they survive navigation: after a step loads a new page, it re-draws whatever is still up. Annotations last until an `annotate` step clears them, their `duration` elapses, or the test context ends.