Creating Replay Vision scanners
A scanner is a standing LLM probe over session recordings. Once created and enabled, it runs on a
Temporal schedule that sweeps every 5 minutes, applying its prompt to each new matching recording and
recording the result as an observation (a queryable
event). Each observation counts
against a
monthly org quota (a fixed number of observations per calendar month).
That schedule is exactly why creation needs a gut-check: a scanner with a permissive query and full sampling
starts consuming quota automatically and can drain the whole month's budget within its first few sweeps.
Creation itself does not check quota — that protection only kicks in at observation time, by which point
the budget may already be gone.
Core principle: size before you ship
Never create an enabled scanner blind. Estimate its volume, check remaining quota, and — when the projected
volume is a meaningful fraction of what's left — show the user the numbers and get confirmation before
creating. This is the heart of the skill; the rest is supporting detail.
The flow
Step 1: What should the scanner do?
Pick a
and write its
. Every type needs a
; the rest is type-specific:
| Type | What it produces | shape |
|---|
| Open-ended observation against a prompt (e.g. "flag rage clicks") | |
| Assigns tags from a fixed label set | {"prompt": "...", "tags": ["tag-a", "tag-b"]}
— needs ≥1 entry; optional , "allow_freeform_tags": false
|
| Numeric score on a rubric | {"prompt": "...", "scale": {"min": 1, "max": 5, "label": "frustration"}}
— < ; optional |
| Free-text summary; optional facet embeddings for search | ; optional "length": "short" | "medium" | "long"
(default ), "emits_embeddings": false
|
is
locked after creation — to change it you delete and recreate, so confirm the type is
right up front, and get the
shape right (a wrong shape is a create error, not a silent
default).
If the user's intent makes the type and prompt obvious, just proceed — don't interrogate them.
Step 2: Which sessions?
The
is a
shape that selects which recordings the scanner watches.
and
are
ignored (the schedule controls time), so don't bother setting them. Narrow the query to the
sessions that actually matter — by event, URL, person property, duration, etc. A narrow query is the single
biggest lever on cost.
(0..1, default 1.0) is a random downsample applied
after the query matches. Lower it to
trade coverage for budget.
Step 3: Size it — the gut-check (do not skip)
Before creating, run both checks and reason about them together:
- Estimate volume — call
vision-scanners-estimate-create
with the proposed + .
It returns matched_sessions_in_window
, the measured, and
estimated_observations_per_month
.
- Check budget — call for and against the org's monthly
.
Then decide:
- If
estimated_observations_per_month
comfortably fits within , proceed.
- If it's a large fraction of (or exceeds) , stop and tell the user the concrete numbers
— e.g. "This scanner is projected to produce ~X observations/month; you have Y of Z left this month." —
and confirm before creating, or suggest tightening the or lowering first.
- If the org is already , say so — a new enabled scanner won't produce anything until the quota
resets, and its observations will be silently skipped.
Confirmation here is a conversation step, not an API capability — surface the trade-off and let the user
choose. When the projected volume is clearly small relative to the budget, you don't need to ask.
Step 4: Create
json
{
"name": "Rage click monitor",
"scanner_type": "monitor",
"scanner_config": { "prompt": "Flag sessions where the user repeatedly clicks the same element in frustration." },
"query": { "kind": "RecordingsQuery", "events": [{ "id": "$rageclick", "type": "events" }] },
"sampling_rate": 1.0,
"model": "gemini-3-flash-preview",
"enabled": true
}
must be unique within the team. Set
if the user wants to create it paused (no
schedule, no quota consumption) and turn it on later.
After creation
- Show the scanner's PostHog URL from the response so the user can review it in the UI.
- Results take a few minutes to appear (rasterizing the recording to video + the LLM call are slow). Inspect
them with
vision-scanners-observations-list
for one scanner over time, or
(requires ) for every scanner's findings on a single session. To dig into a recording, hand off
to the skill.
Updating an existing scanner
is a partial update — send only changed fields.
Re-run the Step 3 gut-check
whenever you widen scope: a broader
or a higher
raises the sweep volume just like a
fresh broad scanner would. Toggling
, tweaking the prompt, or narrowing the query don't need a
re-estimate. Editing config bumps
; past observations keep a snapshot of the old config.
Gotchas
- One observation per (scanner, session). Re-running a scanner on a session it already observed — even a
failed or ineligible one — is a no-op and won't produce a fresh scan.
- Ineligible ≠ failed. Observations can land (e.g. , ) — a terminal
non-error outcome. Check when triaging why a scanner produced nothing.
- Provider/model are Google/Gemini only in the current version.