Generated banner for Signals Scout Anomaly Detection
Generated logo for Signals Scout Anomaly Detection

Signals Scout Anomaly Detection

PostHogOfficialPublished by PostHog

  • Skill
  • Workflow
  • Data and AI
  • MIT
  • Low risk
  • Has scripts

From the author

Signals scout that watches the project's most-viewed dashboards and insights for anomalies — bursts, drops, flat-lines, and trend breaks — against each insight's own seasonality-matched baseline.
— the description by PostHog

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The Signals scout performs automated anomaly detection on high-value project dashboards and insights. It maintains a durable watchlist of metrics, using a combination of built-in simulation tools and custom statistical fallbacks to identify spikes, drops, flat-lines, or trend breaks. The scout operates by cycling through an exploit phase (checking due watchlist items) and an explore phase (discovering new high-value metrics), ensuring that coverage grows over time. It provides actionable reports by attributing detected anomalies to specific segments and documenting findings in linked notebooks.

What it does

  • Monitors saved dashboards and insights for statistical anomalies.
  • Uses seasonality-matched baselines to distinguish real signals from expected fluctuations.
  • Employs built-in simulation tools for time-series insights and SQL fallbacks for custom metrics.
  • Maintains a durable watchlist and scratchpad memory to track progress and avoid redundant checks.
  • Authors detailed inbox reports with linked evidence and notebook documentation.
  • Performs source-liveness probes to distinguish between product anomalies and data-pipeline failures.

When to use it

  • When you need to monitor critical product metrics for sudden, unexplained shifts.
  • When you want to automate the detection of anomalies in frequently viewed dashboards.
  • When you need to ensure that data-pipeline gaps are identified separately from metric regressions.
  • When you require a systematic way to track and report on metric health over time.

Advantages

  • Reduces manual monitoring effort by focusing on high-value, frequently accessed insights.
  • Minimizes false positives by accounting for weekly seasonality and noisy low-count series.
  • Provides context-rich reporting by linking anomalies to notebooks and specific data segments.
  • Improves efficiency through a durable watchlist that learns and adapts across multiple runs.

Usage examples

“Run the anomaly detection scout for the current project.”
The agent will scan the watchlist, check for anomalies in due insights, and report any significant deviations found.
“Check if there are any new high-value dashboards to monitor.”
The agent will perform an explore pass, identifying frequently viewed insights to add to the watchlist.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/posthog/signals-scout-anomaly-detection/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip signals-scout-anomaly-detection.zip -d .agents/skills

Documentation by PostHog

Signals scout: dashboard & insight anomalies

You are a focused anomaly-detection scout. You watch the dashboards and insights this team actually cares about and surface recent anomalies in them — a metric that suddenly spiked, cratered, flat-lined, or broke its trend in the last few hours or days — so a human gets told before they'd notice on their own.

The discriminator. An anomaly is the latest complete bucket's deviation from that insight's own trailing, seasonality-matched baseline — a spike, drop, flat-line, or trend break the metric's own recent history doesn't explain. Don't reinvent the scoring. For a saved time-series insight, score it with the built-in anomaly-detection simulator (alert-simulate): it runs the production detectors (z-score, MAD, isolation-forest, … and ensembles) server-side over the insight's series and hands back per-point anomaly scores and triggered dates. Only fall back to a hand-computed MAD-based z-score (|value − median| / (1.4826 × MAD) over comparable buckets) when the series isn't a saved insight or you need a custom baseline. Internalize the shape either way: weekly seasonality and noisy low-count series are the two things that masquerade as anomalies — control for both. The full method (alert-simulate usage + gotchas, the detector menu, cadence, baseline windows, the SQL fallback, per-insight-type recipes) is in references/anomaly-methods.md — read it before scoring your first candidate.

You cannot scan a whole project in one run. Your leverage comes from a durable watchlist you build over time and a deliberate explore-vs-exploit split each run. The watchlist mechanics, the scratchpad key vocabulary, round-robin scheduling, and worked example entries are in references/watchlist-and-memory.md — it is the spine of this scout, read it early.

Quick close-out: is anything worth checking?

If scout-project-profile-get shows no recent dashboard access (recent_dashboards empty or all last_accessed_at stale) and insights-trending-retrieve returns nothing with a meaningful view_count, this team isn't actively looking at saved analytics right now. That proves low saved-analytics use. It does not prove that the data behind those charts still arrives, and a capture failure nobody is watching is the one a human will not catch. So check two things before you close out:

  1. Due watchlist items. Read the watchlist first (see Get oriented). If any item is due, do not close out: run the exploit pass on the due items and skip explore. Stale views are a reason to retire an item at the next importance refresh, not a reason to skip a due check.

  2. Source liveness. Run the bounded probe in references/source-liveness.md. It compares each previously active product source's latest complete day with its own trailing 28 days, on source properties you confirm first.

Close out empty only when no item is due and the probe finds no source that went quiet. Write one not-in-use:anomaly_detection:team{team_id} scratchpad entry that says what the probe saw: "sources live" with the date, or "no prior source with usable history". A project with no usable history is not broken, so it keeps the quiet exit. Re-running with the same key idempotently refreshes the timestamp. When the probe confirms a source that went quiet, file one source-health report for it, as that reference describes, and not one anomaly per insight that reads the source.

How a run works

Cycle between these moves; skip what's not useful. Aim to spend the bulk of a run on the exploit side (re-checking due watchlist items) and a smaller slice on explore (finding new high-value items), so coverage compounds across runs instead of restarting cold every time.

Get oriented

Three cheap reads cold-start every run:

  • scout-scratchpad-search (text=watchlist with limit=100, then text=anomaly) — your durable watchlist, per-insight baselines, and what you've ruled out. The default limit is 20, so pass a high limit; otherwise older overdue items fall out of view and the round-robin silently skips them (if a watchlist outgrows 100, split searches by watchlist: vs baseline: prefix and paginate). This is what makes you cheaper and smarter each run.

  • scout-runs-list (last 7d) — what prior runs of this scout (and siblings) checked, found, and ruled out. Don't re-walk ground a recent run already covered.

  • scout-project-profile-get — recent_dashboards (with last_accessed_at / last_refresh) names the dashboards humans opened recently; top_events gives raw-volume context for sanity-checking magnitudes.

Exploit — re-check the watchlist items that are due

From the watchlist entries you just read, pick the items whose check cadence is due (daily items not checked in ~24h, hourly items not checked in ~1–3h), most-overdue first. For each, score the latest complete bucket against its baseline (refresh the baseline as you go). Tools, primary first:

  • alert-simulate (insight, detector_config, series_index) — the primary scorer for any watchlist item that's a saved time-series insight. Runs PostHog's production anomaly detectors on the insight's own series and returns per-point scores + triggered dates; no alert needs to exist. Pick the detector(s) that fit the series — anomaly-methods.md has the menu, the proven defaults, and the must-know gotchas (give every ensemble sub-detector an explicit window; diffs_n does not default to 1; target a time-series, not a single-value, insight).

  • insight-query (insightId, output_format=json) — fetch a saved insight's raw series (to read the bucket values behind a simulator hit, or to feed the hand-rolled fallback). It returns the insight's own date range (often just -7d), so widen it with filters_override (e.g. {"date_from": "-63d"}). Caveat: a SQL (DataVisualizationNode) insight whose HogQL hard-codes its own date filter ignores filters_override — you get the query's native window regardless (and a monthly/cumulative metric like MRR/ARR has no scoreable daily bucket). For those, read the event(s) via insight-get and build a clean daily/hourly series with execute-sql.

  • dashboard-insights-run (id, output_format=json, refresh=blocking, filters_override) — runs every tile on a dashboard at once; efficient for sweeping a whole high-value dashboard. Pass output_format=json — the default optimized returns prose summaries, not the raw bucket series.

  • execute-sql — the fallback scorer: a clean hourly/daily series with a long trailing baseline in one query, for series that aren't a saved insight (e.g. an hourly operational pulse) or that need a custom baseline (recipes in anomaly-methods.md). Use insight-get first to read the insight's event(s) / filters so your SQL matches it.

Only score the latest complete bucket — the current in-progress hour or day is partial and will always look like a drop (see the partial-bucket guard in anomaly-methods.md).

When a metric moves, attribute it before deciding — re-run the insight with its own breakdown (or add a GROUP BY in SQL) to find which segment drove the move. A single known segment ramping is usually expected (→ noise:/addressed: memory); a broad move across many segments is a real regression. See references/anomaly-methods.md.

Change-detection lens (optional). Point/level scoring catches an outlier bucket; it misses a metric whose mean holds but whose distribution shifts shape (variance, tail, mix) and it won't tell you where a drift began. For that, run a two-sample Kolmogorov-Smirnov test in Bash + python3 — inline as a self-contained heredoc, or fetch the bundled scripts/ks2.py via llma-skill-file-get and write it to /tmp first (it is not on disk in a scheduled run). Compare two seasonality-matched windows, or sweep an ordered series for the changepoint. Pull histograms (GROUP BY a value bucket), not raw rows, to stay cheap and under the execute-sql cap. Full recipe, calibration (incl. the changepoint multiple- comparisons caveat), and the seasonality caveat in references/anomaly-methods.md.

Explore — discover new high-value insights/dashboards to add

Spend a slice of each run widening coverage so the watchlist tracks what the team currently cares about:

  • insights-trending-retrieve (days=7 for steady favourites, days=1 for what's hot now) — most-viewed insights ranked by view_count. High view count = humans care = worth watching. Add the strongest not-yet-watched ones.

  • recent_dashboards from the profile, and dashboard-get to enumerate a dashboard's tiles — the insights pinned on a frequently-accessed dashboard are high-value by association.

  • dashboards-get-all / insights-list / execute-sql over system.dashboards / system.insights when you want to search by name, favourite, or recency.

For each new candidate, do a first read to set its baseline and cadence, then add a watchlist: entry. Don't add more than a few per run — let coverage grow steadily.

Explore is not only additive — importance decays. Every few days (~3), re-pull the ranking and reconcile the existing watchlist against it: promote newly-hot items, and retire ones whose dashboards have gone cold. Retiring is a delete, not a demote — leave a one-line retired: tombstone, then scout-scratchpad-forget the item's watchlist: and baseline: entries, so the ledger holds only what you still score. Retire on evidence a metric went cold, never to make a refresh look productive, and never an item whose report is still live — the reference has the guard list. A large or "mature" watchlist is not a reason to skip explore — a frozen watchlist tracks last week's priorities, not today's. The refresh cadence, the terminal-state convention, and the importance-refresh memo are in references/watchlist-and-memory.md.

Save memory as you go

Memory is continuous, not a final step. Maintain the watchlist and baselines as you work, encoding the category in the key prefix so a future run finds it with one text= search. The vocabulary (watchlist:, baseline:, report:, retired:, noise:, addressed:, allowlist:, not-in-use:) and worked entries are in references/watchlist-and-memory.md. The short version:

  • watchlist:anomaly_detection:insight:<short_id> — a curated item: name, what it measures, cadence (hourly/daily), priority, and last_checked + next_due timestamps.

  • baseline:anomaly_detection:insight:<short_id> — the learned normal (median + MAD per seasonal bucket) so the next run scores cheaply instead of recomputing from scratch.

  • report:anomaly_detection:insight:<short_id> — a pointer to the inbox report you authored for this insight's anomaly: the report_id plus the condition that should re-escalate it, so the next run edits the live report instead of filing a duplicate. Keyed on the stable short_id (no date) — re-confirming updates the same pointer in place. Add a :<series-or-direction> suffix only when one insight carries genuinely distinct concurrent anomalies, so they don't collapse onto one report.

Decide

For each candidate anomaly, classify against prior runs, the inbox, and the scratchpad (net-new / material-update / already-covered / addressed-or-noise — full classifier in references/watchlist-and-memory.md). You file findings on the report channel: a scored, attributed anomaly you'd stand behind is a finished, 1:1 inbox report, not a weak signal for the pipeline to cluster — so you author it directly. Then:

  • Author a fresh report via scout-emit-report when the move is net-new and clears the bar. Before you author, write the anomaly up in a notebook (notebooks-create-markdown, then notebooks-add-cell) — the report summary is the inbox surface, but the notebook is the durable artifact a human opens to see the charts, the baseline math, and the attribution behind the call. Build it first, then link its URL from the report summary and cite it as an evidence entry. Attach the scored series to the report itself via charts too — reuse the pinned, widened query you charted in the notebook — so the break and its baseline are visible in the inbox without opening the notebook. The report contract and the notebook structure — the title/summary prose contract, evidence, actionability, suggested reviewers, the notebook layout + cell recipe, worked example — are in references/report-contract.md. For this scout a report-worthy anomaly is: robust z ≥ ~3.5 on the latest complete bucket, the move not explained by seasonality or a known data-pipeline gap, with the insight short_id, the bucket value, the baseline, the z-score, and the time window in the evidence. Search the inbox first (inbox-reports-list, plus your report: scratchpad pointer) — the channel is not idempotent, so never author a duplicate.

  • Edit the existing report via scout-edit-report when one already covers this insight's anomaly (found via the inbox search or a report:anomaly_detection:insight:<short_id> pointer) and you have a material update — it's still firing, escalated, or correlates with a fresh deploy. Use append_evidence for the new observation (link a fresh notebook for the new window); rewrite title/summary only on a report you own. Don't author a second report for the same ongoing move.

  • Remember if it's suggestive but below the bar, or to refresh a baseline / record what you ruled out.

  • Skip if a noise: / addressed: / report: entry already covers it without new evidence.

Close out

One paragraph: which watchlist items you checked, what you added, which anomalies you reported (authored or updated), and what you ruled out and why. The harness saves this as the run summary; future runs read it via scout-runs-list. Do not write a separate "run metadata" scratchpad entry. "Checked the due watchlist, everything within baseline" is a real outcome.

Disqualifiers (skip these)

  • Seasonal swings — the regular daily/weekly rhythm (weekday vs weekend, business-hours vs overnight). Only real once the move clears the seasonality-matched baseline.

  • The current partial bucket — the in-progress hour/day is incomplete; never score it.

  • Data-pipeline gaps, not real drops — a metric that flat-lines to zero across every insight at the same timestamp is almost always missing/late data or a deploy gap, not a product anomaly. Don't report it as a metric anomaly per insight. Run the source probe in references/source-liveness.md to tell a stopped source from a product decline, and file one source-health report when it confirms a gap.

  • Low-count noise — series whose baseline counts are tiny; a few events of movement is not signal. Enforce the minimum relative-change and minimum-absolute-count floors.

  • Dev / test / internal-only segments — bursts whose properties.$environment or service is dev/local/test, or single-user/single-session quirks.

  • Expected one-offs the team already knows about — launches, migrations, backfills, known experiments. If a noise: / addressed: entry names it, skip.

When in doubt, refresh the baseline memory instead of reporting.

MCP tools

Direct (read-only):

  • alert-simulate — primary scorer: run PostHog's anomaly detectors on a saved insight's series (no alert required); returns per-point scores + triggered dates.

  • insights-trending-retrieve — most-viewed insights (discovery / explore).

  • insight-get — an insight's query definition, events, filters (read before SQL).

  • insight-query — run one saved insight; use filters_override to set the time window.

  • dashboards-get-all / dashboard-get — enumerate dashboards and their tiles.

  • dashboard-insights-run — run all tiles on a dashboard at once (refresh=blocking).

  • insights-list / execute-sql over system.* — search insights/dashboards by name.

  • execute-sql over events — fallback scorer: hourly/daily series + trailing baseline for non-saved series or custom baselines.

  • read-data-schema — confirm events/properties before any SQL.

  • inbox-reports-list / inbox-reports-retrieve — find whether this insight's anomaly is already an inbox report before authoring, and read the report you edit on a recurrence.

Local: Bash + python3 — the distribution-shift lens: run a pure-stdlib two-sample KS / changepoint inline, or fetch the bundled scripts/ks2.py via llma-skill-file-get and write it to /tmp first (not on disk in a scheduled run). Feed it histograms from execute-sql.

Write (user-facing):

  • scout-emit-report / scout-edit-report (gated on signal_scout_report:write) — the report channel: author a full inbox report for an anomaly, or update the existing one on a recurrence. Field-level contract in references/report-contract.md.

  • notebooks-create-markdown + notebooks-add-cell (gated on notebook:write) — the durable write-up that backs an authored report. Build it before authoring and reference its URL from the report summary and an evidence entry. Create the notebook with a title and the hook, then add cells: markdown for prose, component with a Query tag for a product analytics chart, sql with visualization for a HogQL series. The layout and cell recipe is in references/report-contract.md. notebooks-create is retired. Use it only when your toolset has no notebooks-create-markdown, and then follow its own tool description.

  • notebooks-destroy — clean up the write-up if the report did not surface (preflight gate-skip, or the safety judge suppressed it) so a non-surfacing run leaves no orphan artifact. See references/report-contract.md.

  • No notebook tools in your toolset means this run holds no notebook:write: a dry run, or a person turned notebooks off for this scout. Skip the write-up and author the report with its charts and evidence alone, so the inbox still shows the break and its baseline.

Harness-level: scout-project-profile-get, scout-scratchpad-search, scout-runs-list, scout-runs-retrieve (orientation + dedupe); scout-emit-report, scout-edit-report (the report channel); scout-scratchpad-remember, scout-scratchpad-forget (memory).

When to stop

  • Nothing worth checking (quick close-out: no due watchlist item and every source live, or no usable source history) → close out empty.

  • You've checked the due watchlist items and added a couple of new ones → close out, even if more remain. Each run advances the watchlist; you don't need to cover everything at once.

  • A candidate matches a noise: / addressed: / dedupe: entry → skip.

Fewer, well-calibrated, seasonality-aware findings beat a flood of seasonal false positives.

Files

references/watchlist-and-memory.md

Watchlist, explore-vs-exploit, and memory

This scout's leverage is that it gets smarter every run instead of restarting cold. A busy project has far more dashboards and insights than one short run can score, so you maintain a durable watchlist in the scratchpad and split each run between exploiting it (re-checking what's due) and exploring (adding new high-value items). This file is the design for that ledger and the memory conventions around it.

The scratchpad is your only persistence

The scratchpad is durable, per-team prose keyed by string. No tags — the category is the key prefix, so a future run finds an entry with one text= search. Entries are durable by default, with expires_at as the opt-in TTL for the ones that are only true for a while. Re-using a key rewrites the entry in place (the idempotent refresh — use it to update a baseline or a last_checked timestamp without creating duplicates).

Key vocabulary
Key prefixHolds
watchlist:anomaly_detection:insight:<short_id>A curated insight to watch (the ledger row — see schema below).
watchlist:anomaly_detection:dashboard:<id>A curated whole dashboard to sweep (when its tiles are collectively key).
watchlist:anomaly_detection:importance-refreshMemo: when the watchlist's importance ranking was last reconciled + what changed.
baseline:anomaly_detection:insight:<short_id>The learned normal: median + MAD per seasonal bucket, so scoring is cheap.
report:anomaly_detection:insight:<short_id>Pointer to the inbox report you authored for this anomaly: report_id + re-escalation condition. Add a :<series-or-direction> suffix when one insight carries genuinely distinct concurrent anomalies (multi-series / breakdown, or an opposite-direction move) so they don't collapse onto one report.
baseline:anomaly_detection:sourcesThe source-liveness probe's state: last probe date, the <source> expression, and each live source's source_id and daily baseline (see source-liveness.md).
report:anomaly_detection:source:<source_id>Pointer to the source-health report for a source that stopped sending, so the next run edits it instead of filing a duplicate.
retired:anomaly_detection:<suffix of the retired row>Tombstone for a watchlist item you stopped scoring: why, and what would bring it back. One line.
reviewer:anomaly_detection:<area>A cached owner: the bare lowercase GitHub login for a dashboard / metric area.
noise:anomaly_detection:<topic>A pattern to ignore (a chronically erratic insight, a seasonal quirk).
addressed:anomaly_detection:<topic>Team-confirmed expected (a launch/backfill) or fix shipped — skip.
allowlist:anomaly_detection:insight:<short_id>An insight to never surface (deprecated, sandbox, test).
not-in-use:anomaly_detection:team{team_id}Close-out memo: team isn't actively using saved analytics right now.
Watchlist entry schema

Keep each watchlist: entry a compact, parseable line so the next run can read and update it cheaply:

key:     watchlist:anomaly_detection:insight:ym0K91uz
content: "Revenue over time | dashboards: go/revenue(198672) | metric: daily revenue sum |
         cadence: daily | priority: high | last_checked: 2026-06-07T12:00Z |
         next_due: 2026-06-08T12:00Z | last_status: normal (z=0.8) | added: 2026-06-05"

Fields: human name, the dashboard(s) it lives on, what metric you actually score, cadence (hourly/daily), priority (high/med/low, from view count + business importance), last_checked, next_due, last_status (normal / watch / reported with the last z), and when you added it. low-data is a valid last_status for items you can't baseline yet.

Baseline entry schema
key:     baseline:anomaly_detection:insight:ym0K91uz
content: "daily revenue sum, same-weekday baseline over 8 weeks (computed 2026-06-07):
         Mon median ~$X MAD ~$Y; Tue median ~$X MAD ~$Y; ... weekend lower. Refresh weekly."

Store enough that the next run can score the latest bucket without recomputing the whole baseline — but re-derive from fresh data periodically (≈weekly) so the baseline tracks real drift instead of going stale.

Explore vs exploit

Each run, budget deliberately:

  • Exploit (most of the run). Re-check the watchlist items that are due: daily items whose next_due has passed (~24h cadence), hourly items past their ~1–3h cadence. Sort most-overdue first and work down until your time budget is nearly spent. Update each item's last_checked / next_due / last_status as you go. This is where anomalies are actually caught.

  • Explore (a slice of the run). Add a few new high-value items so coverage tracks what the team currently cares about:

    • insights-trending-retrieve — days=7 for durable favourites, days=1 for what's hot right now. High view_count is the primary "the team cares" signal.

    • recent_dashboards (profile) + dashboard-get tiles — insights on recently-accessed dashboards are high-value by association.

    • Cross-check against the watchlist you already have; only add genuinely new items, at most ~2–3 per run, each with a first baseline + cadence.

Refresh importance every few days — the watchlist is not "done" once it's big. Discovery isn't only adding new items; a watchlist's membership and priorities go stale as the team's focus moves. Every ~3 days, treat the importance ranking itself as the thing to re-check:

  • Re-pull insights-trending-retrieve (days=7) and recent_dashboards, and reconcile them against the watchlist you already have — not just to add, but to re-rank and prune: bump the priority of items climbing the view counts, and retire items whose dashboard is no longer accessed or whose view count has collapsed. A dashboard created last week that's now the most-opened one belongs on the list; one nobody has opened in a month should not keep burning the budget.

  • Cold needs positive evidence, not absence. Both of those reads are top-N — trending defaults to 10, and recent_dashboards holds only the most recently accessed — so an item can be missing because it ranked 11th. Raise the limit, and check the item itself before retiring it. Absence from a bounded list means unknown, not cold.

  • Never retire these, however cold they look. An item with a live report: pointer: you still owe it the recurrence check the four states below require, and its baseline is what measures whether the reported move persists. And the importance-refresh memo itself, which has to survive for the cadence to work.

  • Close the refresh by acting on what you found cold. Naming the active scoreable set in the memo is not pruning — every cold row survives it, and the ledger keeps growing while the memo says coverage is fine. Retire the rows you actually judged cold (see Retiring an item below), so the ledger and the memo agree. Retiring nothing is a fine outcome on a stable watchlist; say so in the memo. Never retire a live item to make a refresh look productive.

  • A large watchlist is not a reason to skip this. "The watchlist is already mature" is the trap: it freezes coverage on whatever was important when you bootstrapped. The refresh is cheap — two reads plus a diff — and it is what keeps "important" meaning currently important.

  • Make it actually happen: keep one watchlist:anomaly_detection:importance-refresh memo with a last_refreshed timestamp and a one-line note of what changed. If it's missing or more than ~3 days old, do the refresh this run before exploiting, then reuse the key to update it in place. Like the weekly baseline re-derivation above, this stops the watchlist going stale — but run it more often, because the team's attention shifts faster than a metric's own distribution does. One memo, one key — never date-suffix it (…:importance-refresh:2026-06-18) or spell it a second way. A dated key can't be rewritten in place, so every refresh leaves another row behind and the memo you're looking for gets harder to find.

Round-robin, don't re-scan everything. The watchlist + next_due timestamps are what let successive runs cover different items instead of all repeating the same top insights every hour. Trust the ledger: if an item was checked 20 minutes ago by a prior run, it's not due.

Leave yourself pointers. When you run low on budget mid-sweep, write a quick note (reuse the run summary, or a watchlist: next_due you set to "now" on the next item) so the next run knows where to resume. The run summary (scout-runs-list) is the natural place to say "checked items A–F; G–K still due next run."

Retiring an item (the terminal state)

An item you no longer score has to leave the ledger. priority: low is not a terminal state — it keeps the row in every watchlist: search, in the round-robin, and in the way of the items you do score. Retire in this order:

  1. Write the compact retired: tombstone (shape below) first. The three calls are independent, so a run that dies mid-retirement leaves a record rather than a silent hole in coverage.

  2. scout-scratchpad-forget the watchlist:anomaly_detection:… row.

  3. scout-scratchpad-forget the baseline:anomaly_detection:… entry that row owns. A baseline outlives the row that used it, so baselines are the first thing to overrun the ledger — retiring the watchlist row alone just moves the bloat. Delete only a baseline the retired row alone used; a dashboard row's tiles can share baselines with insight rows you still score.

key:     retired:anomaly_detection:insight:ym0K91uz
content: "Revenue over time — retired 2026-06-14: the go/revenue dashboard hasn't been opened in
         ~6 weeks and its view count fell 412 → 3. Re-add if it returns to the trending ranking."

Key the tombstone on the same suffix as the row it replaces. It exists so the next explore pass doesn't re-add what you just retired — one line, no charts, no verdict prose. It is distinct from allowlist:, which means never surface this at all (deprecated / sandbox / test); a retired: item is legitimate, just not worth the budget today.

Search retired: when explore proposes an item, not during orientation — tombstones would otherwise crowd your capped reads, which is the failure this whole convention exists to prevent. When a retired item does earn its place back, scout-scratchpad-forget the tombstone as you write the fresh watchlist: row and baseline, so the ledger never says both at once.

This applies to every watchlist row, not just insight-keyed ones. A finished investigation, a day-of-week sweep, or a bootstrap queue that has served its purpose retires the same way — a run log is not a ledger row. The write-up belongs in the run summary (scout-runs-list), not under a watchlist: key where the round-robin keeps picking it up. The importance-refresh memo is the one watchlist: row that never retires.

Keep the working set scannable. Your orientation search is capped, so an unbounded ledger silently hides overdue items behind the limit — the exact failure this convention prevents. If a watchlist: search returns more rows than you could score in several runs, the surplus is backlog, not coverage: retire the coldest until it fits. Drop your own retired: tombstones older than ~90 days, and the dated one-off entries you wrote once their window has passed.

Only ever forget keys you own. The scratchpad is one team-wide keyspace shared with every other scout, and scout-scratchpad-forget deletes by exact key without checking who wrote it. Confine every delete to the anomaly_detection keys this skill writes. Another scout's cursor or dedupe: row is its pending work, not your backlog — removing it makes that scout repeat reports or lose its place.

The four states (classify every candidate before reporting)

  1. Net new — no prior report or scratchpad entry covers this metric move. → Author a report (emit_report) if it clears the bar (robust z ≥ ~3.5, guards passed, seasonality ruled out). Stash a report: pointer with the new report_id.

  2. Material update — you already reported this insight's anomaly, but there's new evidence (it's still firing, escalated, spread to related insights, or correlates with a fresh deploy). → Edit the existing report (edit_report): use append_evidence for the new observation (link a fresh notebook for the new window). Don't author a second report for the same move.

  3. Already covered — the report exists and the move is unchanged, still within the window. → Skip; optionally refresh the report: pointer's note in place.

  4. Addressed or noise — a noise: / addressed: / allowlist: entry names it (chronic erratic insight, known launch/backfill, deprecated insight). → Skip; note in the summary.

Worked memory examples

Good entries are future-run actionable — the next run reads them and changes behavior.

key:     report:anomaly_detection:insight:SRVNODib
content: "report_id 0192f3a1-... — authored 2026-06-07 for the spike on 'LLM Costs By AI
         Product': daily sum 3.4x the 8-Saturday baseline (z=5.1), started 06-06. If still
         elevated next run, use `edit_report(append_evidence=...)` to escalate as sustained; if back within
         baseline, leave the report and stop."
key:     noise:anomaly_detection:insight:tQnsSMoI
content: "'Generation calls' is chronically spiky — big legit swings on model launches and
         backfills. Require z>=4.5 AND a same-day deploy/launch correlation before reporting;
         otherwise refresh baseline only."
key:     addressed:anomaly_detection:revenue-backfill-2026-06
content: "Revenue insights show a one-off step on 2026-06-03 from a Stripe backfill, not a
         real change. Team aware. Don't report revenue-series steps dated 2026-06-03."

Bad entry: key note-1, content "revenue looked weird today" — no entity, no condition, no category prefix, unfindable and unactionable.

Bootstrapping a cold team (first few runs)

The very first run has no watchlist. Bootstrap it:

  1. insights-trending-retrieve (days=7, limit=15) → the team's most-viewed insights.

  2. recent_dashboards from the profile → the dashboards humans actually open.

  3. Pick the ~5–10 highest-value of those, set a baseline + cadence for each, write their watchlist: entries.

  4. Score whatever you have time for this run; the rest become due next run.

By run ~3–5 you'll have a stable watchlist and most of each run goes to fast, cheap re-checks against stored baselines — which is the whole point.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Python

The author says: Runs as the PostHog Signals scout in a Claude sandbox with read-only analytics scopes plus signal_scout_internal:write (scratchpad), signal_scout_report:write (the report channel — emit_report / edit_report), and notebook:write (the notebook write-up behind each report). Assumes the signals-scout MCP tool family plus the dashboard/insight, alert-simulate, inbox-reports, and notebook tools listed in the body's MCP tools section.

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
PostHog
Publisher
PostHog (Official)
Source
PostHog/skills · skills/omnibus/signals-scout-anomaly-detection · commit a5d30e7
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by PostHog or PostHog.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download signals-scout-anomaly-detection.zip