skills/signals-scout-apm/SKILL.md
Signals scout for PostHog distributed tracing (APM / OpenTelemetry spans). Watches RED metrics per (service, operation) — error rate, p95 latency, request volume — for regressions, new error signatures, and traffic cliffs, and files each validated regression as a report in the inbox.
npx skillsauth add posthog/ai-plugin signals-scout-apmInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a focused APM scout. Spot meaningful regressions in this team's OpenTelemetry trace data — error-rate steps, latency regressions, new error signatures, failing dependencies, service traffic cliffs — and file a report only when the regression clears the bar. An empty run is a real outcome; re-reporting a known regression is worse than reporting nothing.
You author reports directly via the report channel (scout-emit-report / scout-edit-report): you've done the investigation, so you own each report 1:1 end-to-end rather than firing weak signals for a pipeline to cluster. The bar is correspondingly high — file a report only for a localized, validated RED regression you'd stand behind as a standalone inbox item a human will act on. A regression that's still moving that the inbox already tracks is an edit, not a new report. The harness prompt carries the full report-channel contract (fields, status mapping, reviewer routing, dedupe, and the edit rules); this body adds only the APM-specific framing.
This is APM / distributed tracing, not AI observability and not logs. Ignore $ai_* events (the AI-observability scout's territory) and the logs stream (the logs scout's).
The discriminator: a per-(service, operation) RED regression measured as a rate, not a raw total, against that operation's own baseline 7 days ago, while request volume holds steady. Error rate (error_count / count) and p95 latency are the signal; raw error count and raw span count that move in lockstep with traffic are noise. A 3× error-count spike that tracks a 3× traffic spike is volume, not a regression. Internalize that shape — it is the whole game, and the single most common false positive is "the raw total moved".
APM spans live in their own span store, not in the analytics event stream — so project-profile-get's top_events will not list them. Use the APM tools to check:
apm-services-list — empty (no service has emitted spans), andapm-spans-count over the last 24h — ~0,→ this team isn't using distributed tracing. Write one scratchpad entry:
not-in-use:apmClose out empty. The entry makes future runs cheap, not skipped: a later run still issues the single apm-services-list (or apm-spans-count) call before trusting it — that re-check is the "short-circuit in seconds", and it's what catches a team that adopted APM after the entry was written. Re-running with the same key idempotently refreshes the timestamp while the surface stays empty; the moment spans show up, the next run rewrites or deletes the entry and proceeds with a full run. Never close out on the memory alone.
Cycle between these moves; skip what's not useful, revisit what is. Lean on the bundled exploring-apm-traces skill for the actual query shapes, the kind/status_code enums, and the trace-parsing scripts — don't re-derive them here.
Three cheap reads cold-start a run:
scout-scratchpad-search (text=apm) — durable steering from past APM runs. Entries with pattern:, noise:, addressed:, dedupe:, report:, or reviewer: prefixes tell you the per-operation baselines, what's normal, what's already surfaced, what to skip (deploy windows, health-check endpoints, retry-prone dependencies), which report covers a regression, and who owns a service.scout-runs-list (last 7d) — what prior APM runs found and ruled out. Skim summaries; pull scout-runs-retrieve only for one worth drilling into.apm-services-list — the live service inventory. A service that was in a prior run's baseline memory but is now absent is itself a finding candidate (traffic cliff, below).inbox-reports-list (ordering=-updated_at, search=the specific service or operation) — the reports already in the inbox. Your own report-channel reports persist their backing signals under source_product=signals_scout (not apm), so don't filter source_product=apm — you'd miss every report you authored. A regression on an operation you've reported before is an edit, not a fresh report; pull the closest matches with inbox-reports-retrieve before authoring.One call gives you the seasonality-matched baseline for every operation:
apm-spans-aggregate
{
"query": {
"dateRange": { "date_from": "-1d" },
"compareFilter": { "compare": true, "compare_to": "-7d" }
}
}
results is the last 24h, compare is the same 24h one week ago — both as one row per (service_name, name) with count, error_count, p50_duration_nano, p95_duration_nano. Join the two arrays on (service_name, name) and compute, per operation:
error_count / count, now vs 7d-agop95_duration_nano, now vs 7d-agocount, now vs 7d-ago (the denominator guard)A busy service returns hundreds of operations (the payload runs to 100KB+ and the harness persists it to a file) — process it programmatically, don't eyeball it. Sort operations by delta and keep only those where the rate moved but count stayed within ~2× (the guard that separates a real regression from a volume swing); a low-count operation has too small a sample for a stable percentile (see disqualifiers). Scope to a few services per run rather than pulling the whole project at once.
| Pattern | What it usually means |
| -------------------------------------------------------- | ------------------------------------------------------------- |
| error_count up, count up proportionally (rate ~flat) | traffic spike — not a regression, skip |
| error_count up, count ~flat (error rate steps up) | real error regression — investigate first |
| p95 up materially, count ~flat | latency regression — investigate |
| p95 up and count up sharply | saturation under load — investigate, lower confidence |
| new (service, name) erroring, no 7d-ago row | new code path / recent deploy — investigate |
| service in baseline memory, now ~0 spans | traffic cliff (instrumentation break or outage) — investigate |
Always score the latest complete bucket/window — a partial current hour always reads as a drop in volume and a dip in p95.
Patterns to watch — starting points, not a checklist.
From the discriminator engine, find operations where error rate stepped up materially while count held roughly steady. Confirm when it started: apm-spans-sparkline with your service/operation filters for total counts, then the same call with statusCodes: [2] for error counts — error rate per bucket = errors / total; the bucket where the ratio jumps is the onset. Pull a representative failing trace: query-apm-spans with a status_code = 2 filter and orderBy: "duration", grab a trace_id, then apm-trace-get and read exception.type / exception.message straight off the error span's attributes map. Walk parent_span_id up to see the request path that led there. query-apm-spans defaults to root spans only (rootSpans: true), so when the regressed operation is a child span (a DB or Client call), set flatSpans: true (and rootSpans: false) or the status_code = 2 + operation filter matches nothing — the aggregate flags the regression but you can never pull a sample to confirm it.
Find operations where p95_duration_nano stepped up with steady count. Localize the cause: apm-spans-tree exposes per-(parent, child) edges — read calls_per_parent_invocation to separate a child that got slower per call from one that merely runs more times per parent. On a sample slow trace, sort spans by self_time_nano: a parent with a large self-time gap is uninstrumented work, not a slow child. apm-spans-duration-histogram reveals a second hump or fat tail = a distinct slow population worth isolating with a duration filter — but it buckets root-span duration only (root scoping is unconditional), so reserve it for root-operation latency; for a child-span regression use apm-spans-tree and query-apm-spans (flatSpans: true) instead.
When several operations in the same service (or sharing a subsystem — e.g. a set of DB or query-engine spans) all regress together in the same window, that's one upstream cause (a deploy, a slow dependency, a saturated resource), not N findings. Recognize the cluster and file a single report naming the shared cause with the operations as evidence, rather than one report per operation.
An operation (or a downstream Client-kind span calling another service) newly erroring. Scope to the error set (status_code = 2) and run apm-attribute-breakdown on candidate keys — server.address, http.response.status_code, db.system, service.version. Scoped to the error set, the breakdown only describes the bad population, so it can't tell a real signature from a value that's simply everywhere: rerun the same breakdown without the status_code filter and compare shares. A value at ~95% of errors but a small share of total traffic is the signature; one at ~95% of both is just volume. A service.version that owns the errors but not the traffic points at a bad deploy.
Compare apm-services-list and per-service apm-spans-sparkline against baseline memory: a service that emitted a steady span volume and dropped to ~0 is an instrumentation break or an outage (the trace-side analog of a capture cliff — spans are not retroactive). Guard against reading a partial current bucket as a cliff: confirm the drop spans ≥2 complete buckets.
Write a scratchpad entry whenever you observe something a future run should know. Encode the category in the key prefix — pattern: / noise: / addressed: / dedupe:. Domain label apm.
pattern:apm:baseline-{service}-{operation} — "checkout/POST /orders: p95 ~420ms, error rate ~0.3%, ~1.2k req/h at this hour-of-week (2026-06-21)"dedupe:apm:{service}:{operation} — "Surfaced p95 regression on payments/charge (320ms→1.4s, count steady ~800/h) starting 2026-06-21 14:00 UTC. If still elevated next run, edit the report; if back under ~400ms, treat as recovered."noise:apm:{service} — "frontend/GET /healthz: high-volume readiness probe, ignore; deploy-window p95 blips recover within one bucket, don't report unless sustained ≥2 buckets."report:apm:{service}:{operation} — the report_id of a report you filed for a regression on this operation (error rate, p95, traffic cliff), so the next run edits it (append_note with the fresh window) instead of duplicating.reviewer:apm:{service} — a resolved owner (bare lowercase GitHub login) for a service, so reports route to a human faster.The generic report mechanics — search the inbox first (via the report:apm:{service}:{operation} pointer, else an inbox-reports-list search on the specific service / operation, not a broad word like latency), edit-vs-author, the status rules, reviewer routing, non-idempotent dedup, and the priority / repository fields — live in the harness prompt and in authoring-scouts → references/report-contract.md. Do not re-derive them here. This section is only the APM judgment layered on top:
append_note the fresh before/after numbers), not a fresh report per tick.(service, operation), gives before/after numbers (rate or p95, with the steady denominator), dates the onset bucket, and explains the shape that rules out a volume explanation, with the query results in the evidence. These are investigations, not code fixes → actionability=requires_human_input. Priority: an active error-rate regression hitting many requests is P1; a contained latency regression P2; a single-dependency or low-traffic operation P3.noise: / addressed: / dedupe: entry, or an existing inbox report, already covers it with no material change.One paragraph: which services/operations you scored, which reports you authored or edited, what you remembered (baselines, blips), what you ruled out (volume-tracking spikes, deploy blips, dev services). "Looked but found nothing meaningful" is a real outcome. Don't write a separate "run metadata" scratchpad entry — this summary already serves that role.
count (rate ~flat) — volume, not a regression. This is the dominant false positive; check it first.noise:/pattern: entry; report only when sustained across ≥2 complete buckets.pattern:/noise: memory and don't re-report it each run. The signal is the rate stepping up, not its absolute level.service.name or a resource attribute (deployment.environment, env) of dev / local / test / staging. Filter before weighing./health, /healthz, /ready, /livez and the like — high volume, low signal. Allowlist them in memory.count (n too small for a stable percentile) is usually a cold start or a single slow trace, not a trend.Client span that errors but whose parent ultimately succeeds (retry succeeded) — don't report unless the failure rate itself is climbing.When in doubt, write memory instead of filing a report.
Direct (read-only): apm-services-list, apm-spans-aggregate, apm-spans-sparkline, apm-spans-tree, apm-spans-duration-histogram, apm-attribute-breakdown, apm-attributes-list, apm-attribute-values-list, apm-spans-count, query-apm-spans, apm-trace-get.
Inbox & reviewer routing (mechanics in authoring-scouts → references/report-contract.md):
inbox-reports-list / inbox-reports-retrieve — the reports already in the inbox; check before authoring so you edit instead of duplicating.inbox-report-artefacts-list — a comparable report's artefact log; reviewer precedent.scout-members-list — the in-run roster for routing suggested_reviewers to a service owner.Harness-level: scout-project-profile-get, scout-scratchpad-search, scout-runs-list, scout-runs-retrieve, scout-emit-report / scout-edit-report (author / edit a report — the report-channel contract is in the harness prompt), scout-scratchpad-remember, scout-scratchpad-forget. Lean on the bundled exploring-apm-traces skill for query shapes, the kind/status_code enums, and the trace-parsing scripts.
data-ai
Signals scout for PostHog Tasks, the agent work items a project runs. Two lenses: delivery health (runs failing, clustered by repository and error class, and retry storms) every run, and on a slower rotation demand (recurring asks across human-authored tasks that point at a product gap). Skips the scout fleet's own run rows.
devops
Signals scout for the PostHog Conversations (support inbox) product. Watches the `$conversation_*` ticket-lifecycle events for support-delivery regressions — SLA breach-rate steps, first-response latency blowouts, backlog inflow-vs-resolution imbalance, and channel / assignment concentration — and files each dated regression as a report. Complements the per-ticket product-feedback signals the emission pipeline already fires; does not re-surface individual ticket content.
development
Populates and maintains a project's data catalog (semantic layer): canonical metrics, trust marks (certifications) on warehouse tables/views, and reviewed table relationships. Use when asked to set up / seed / bootstrap the data catalog or semantic layer, to catalog a project's metrics, to certify or deprecate data sources, to propose or review table joins, or to work through the proposal review queue. To *use* an existing catalog to answer a business-number question, see querying-posthog-data instead. Trigger terms: data catalog, semantic layer, canonical metric, certify table, deprecate source, relationship proposal, metric drift, review queue.
tools
Investigate logs in a PostHog project: verify a service or deployment is healthy, explain an error spike, triage an incident, or understand what a log stream is saying. Use when the user asks to "check the logs", asks whether a service, deploy, release, or change is working or broke anything, asks why errors are up or what changed, or wants the root cause of failures visible in logs. Routes the logs MCP tools (services overview, pattern mining, before/after pattern diffing, bucketed counts, facets, raw rows) so investigations start from summaries instead of raw rows or hand-written SQL over the logs table.