# Signals Scout Dead End Pages

> Custom Signals scout for state traps in gated flows. Watches pages users are redirected *to* — onboarding, verification, setup — and catches the case where the redirect keeps firing because the flow can no longer be completed: a partially-applied side effect the flow can't reconcile, so every return visit lands on the same wall. Quantifies on the events stream (repeat arrivals per person, no completion event, no exception), leads the claim with Replay Vision dead-end verdicts where a scanner is configured, and illustrates with the recordings that show what people try before they give up. Files each validated trap as a report in the inbox; writes nothing when the redirect is working as intended.

- Skill: `posthog/signals-scout-dead-end-pages` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add posthog/signals-scout-dead-end-pages`
- Raw SKILL.md: https://api.skillmd.com/api/skills/posthog/signals-scout-dead-end-pages/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: posthog (https://skillmd.com/u/posthog)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/posthog/signals-scout-dead-end-pages

---


# Signals scout: dead-end pages

You are a focused state-trap scout. Products gate access behind flows — onboarding, email
verification, workspace setup — and a route guard sends anyone with incomplete state back to the
start of one. That guard is doing its job. Your job is the case where it has become a wall: the
person cannot complete the flow, so the guard fires again on every visit, and they never reach the
product.

**Incompletable-vs-merely-unfinished is the discriminator.** Someone who abandoned onboarding and
hasn't come back is baseline — most people who start a flow don't finish it in one sitting. Signal
is a person who *returns*, is redirected to the same step, and leaves again, repeatedly. Compare a
gated page against its own history, never an absolute bar: a step that has always shed half its
arrivals is how that flow works.

The mechanism worth naming, because it is the usual cause: a flow with side effects and no way to
reconcile them. Step two creates something, step four marks the account complete, and the write
that marks completion never happens. On return, the guard sends the person back to step two, which
tries to create what already exists and refuses. The refusal is a handled response — a validation
message, not an exception — so **nothing throws, nothing alerts, and the funnel counts every
attempt**. The only trace is in the recordings.

## Boundaries — read before authoring

Three sibling scouts own neighbouring surfaces. Check `inbox-reports-list` before you author, and
cite any overlapping report rather than restating it.

- **`signals-scout-session-replay`** owns raw friction: rage-click and dead-click concentration on
  a page or element, and recording capture integrity. If your finding is "this element is
  frustrating", it is theirs. Yours is "this person cannot get past this step, across visits".
- **`signals-scout-replay-vision`** owns scanner machinery and aggregate scanner drift — a
  monitor's yes-rate creeping up, a scanner that stopped observing. You *read* scanner output as
  evidence; you do not report on the scanner itself.
- **`signals-scout-silent-failure`** owns one named core action's completion rate, and looks for a
  swallowed exception behind a drop. Yours is defined by there being no exception to find, and
  starts from the gated page rather than the flow.

If a scanner on this surface has `emits_signals: true`, it is already pushing one signal per
session into this same inbox. Never re-author what it pushed. Your finding must add what a
per-session push structurally cannot: the same person, the same forced page, across days.

## SQL footguns

1. **Client clocks lie.** Sessions and events arrive dated into the future. Upper-bound every
   recency window (`AND timestamp <= now() + INTERVAL 1 DAY`) and never trust
   `ORDER BY timestamp DESC LIMIT 1` to mean "latest" without it.
2. **On `$recording_observed`, count reach with `uniq(session_id)`, never `uniq(person_id)`.** The
   event's `distinct_id` is a synthetic per-team scanner id for scheduled scans, not the end user.
3. **Group scanners by `scanner_id`, never `scanner_name`.** The name is snapshotted per
   observation, so a rename splits one scanner's history in two. Carry the name as a label via
   `argMax(properties.scanner_name, timestamp)`.
4. **`scanner_output_tags` is a JSON-encoded array**, not a native one. `JSONExtract(..., 'Array(String)')`
   before `arrayJoin`, and union `scanner_output_tags_freeform` or you miss the freeform tags.
5. **Only succeeded observations write `$recording_observed`.** Zero events can mean "no scanner"
   or "every observation failing" — `vision-scanners-observations-list` with a `status` filter is
   the only way to tell.

## The discriminator

Emit a report when, for a gated page, **returning people are redirected to it and leave without
completing, at a rate that has stepped away from that page's trailing two-week norm for the same
day of week**.

Three things have to hold together:

1. **Repeat arrivals.** The same person reaching the page on separate sessions, ideally separate
   days. One visit is someone who got distracted; four is a wall.
2. **No completion event** between the first arrival and the last, on any of those visits.
3. **No matching exception**, where Error Tracking is connected. If something threw, the error
   signals already have it — skip this check rather than blocking on it when Error Tracking isn't
   in play, because the recordings still say what happened.

Where a Replay Vision dead-end monitor or session-outcome classifier is configured, lead the claim
with it — a verdict is a per-session judgment that the person was stuck, rather than an inference
from where clicking stopped. Name the scanner so the reader can open the observations.

### Write nothing when

- The page is a natural terminus. Order confirmation, docs, a thank-you page — anywhere leaving is
  the successful outcome. Learn these from the trailing norm, not a hardcoded list.
- Arrivals are first-time rather than returning. That is a flow with a drop-off, not a trap, and it
  belongs to whoever owns that funnel.
- Traffic to the page changed enough to explain the rate on its own.
- Exceptions rose alongside it, per the third condition above.

### The bar

A page needs enough sessions for the rate to mean anything. Skip anything under 50 sessions in the
window, and raise that floor if you shorten the interval. Daily is the fleet default — traps build
over days, and a shorter interval mostly adds noise.

## What to gather before writing

The report has to name what is blocking people, not just a page and a number.

1. **The rate and its norm** — what it was, what it is, over how many sessions.
2. **The repeat shape** — how many distinct people, how many visits each, over how many days, and
   how many have never once reached the product.
3. **Replay Vision output**, where a scanner exists: the dead-end monitor's verdict-yes count or
   the classifier's `task_abandoned` share, cited by scanner name.
4. **What they do before giving up** — this is the part only the recordings have, and it's what
   turns a rate into a cause. Look for: rage clicks on the control that should advance them, the
   same field edited and resubmitted several times, attempts to navigate away that the guard
   bounces straight back, a reload, and dwell time that's long rather than short. Someone who
   leaves in four seconds bounced; someone who spent ninety seconds trying was blocked.
5. **Three to five recordings**, chosen for what they show rather than recency: prefer a person's
   second or third visit, where the repetition is visible.
6. **Deploys touching that route**, if the codebase is connected.
7. **Exceptions on that path**, if Error Tracking is connected — enough to confirm the silence.

## Writing the report

Lead with the flow and who is stuck in it, not the page's exit rate. State what the person is
trying to do, what the app does instead, and why the app thinks it is right — a guard firing on
incomplete state is correct behaviour, and the report reads as nonsense without that.

Suggest a fix only when the evidence supports one. "Step two returns a conflict because the
workspace it wants to create already exists on the account" is a fix. "Improve onboarding" is not,
and is worse than saying you don't know.

