Insights
Insights are the things Neens notices for you: error-rate and latency anomalies, eval-score regressions, spiking failure modes, novel failure patterns. Detectors run on a schedule, deduplicate what they find, track each signal’s lifecycle, and deliver it where you’ll see it — the in-app Insights page, Slack, an outbound webhook, or a daily digest email.
At a glance
| Where | The Insights page — a Fleet briefing at the top plus three feeds (Observe / Diagnose / Fix) |
| Key API routes | GET /insights/observe, GET /insights/diagnose, GET /insights/fix, GET /insights/summary, POST /insights/summary/briefing, GET /insights/summary/briefing/{id}, POST /insights/refresh, POST /insights/{id}/feedback |
| When detectors run | Nightly at 02:30 (server clock), and on demand via the page’s Refresh action |
| Scope | Cross-agent, always limited to the orgs/agents you can access |
| Needs | Nothing for detection or for the briefing’s numbers; the AI-written briefing and the digest need an LLM connection and an email provider respectively |
What the detectors watch
Each detector compares a fixed recent 24-hour window against the 24 hours before it, per agent. The thresholds are deliberately simple and documented:
| Detector | Watches | Fires when |
|---|---|---|
| Anomaly | Error rate and p99 latency, per agent + agent name | Recent error rate is at least 2× the baseline and at least 5% — or recent p99 latency is at least 2× baseline and at least 2 s. Windows with fewer than 5 traces are ignored. |
| Regression | Eval pass rate, per agent + metric | Pass rate drops by 5 points or more vs the prior window, with at least 5 scored targets on each side. A drop of 15+ points is high severity. |
| Issue spike | Classified failure-mode (“issue”) volume, per agent + issue | Recent classified sessions reach at least 2× the baseline, with at least 3 sessions (10+ is high severity). Muted/resolved issues never fire. |
Two more insight types are surfaced on demand rather than by the windowed pass:
- Novel failure — a trace that doesn’t match any known failure cluster (a candidate new failure mode), surfaced by the online cluster assigner.
- Pre-prod regression — a pre-prod evaluation run where the candidate version failed prompts the baseline passed.
The three feeds
The Insights page mirrors the platform’s pillars:
- Observe — a fleet-health line (traces, error %, p99, spend) plus the open anomalies.
- Diagnose — the top problems, ranked and capped at 20: failure clusters, open issues, and score regressions, each linking to the right drill-down. See Failure clustering.
- Fix — ranked remediation recommendations drawn from failure clusters (by confidence × sessions affected, capped at 20), linking to the evidence and to Remediations.
A scope picker narrows all three feeds to all agents, one org, or one agent; the header time picker (default: last 24h) sets the window for the fleet stats and issue rollups. Everything is intersected with your access — asking for an agent you can’t see just drops it.
Refreshing
Detectors run nightly, and the page’s Refresh action (POST /insights/refresh) runs them immediately over your accessible scope — useful right after ingesting new data. Refresh also re-fetches all four sections and asks for a fresh briefing narrative, because new detector output changes the story the briefing is describing.
The fleet briefing
At the top of the Insights page, under the Fleet briefing masthead, is a short newspaper-style lede over the current window: a headline, two or three sentences, and four KPI tiles — Traces, Error rate, p99 latency, Spend.
There are two ways that lede can be written, and the page always shows you which one you’re reading:
- a computed briefing, assembled from the real numbers with no model involved. It is always available, it is never blank, and it is what you see first, every time;
- an AI-written briefing over the same numbers, produced by your agent’s LLM connection in the background and swapped in when it’s ready.
What loads when
Nothing on the page waits for anything else. Each of the four sections is on its own clock and paints the moment its own data lands, behind a placeholder shaped like its real content.
Immediately
GET /insights/summary returns without calling a model, so the computed briefing and the KPI tiles appear as fast as your numbers can be aggregated. The tiles fill from whichever of the briefing or the Observe feed answers first — they carry the same four figures.
In parallel
Observe, Diagnose and Fix stream in independently. A slow or failing feed shows a retryable error in its own section and leaves the rest of the page working.
In the background
If an AI briefing is worth writing, the page asks for one and polls for it. While it is being written you get a small “Neens AI is writing a fuller briefing…” note next to the by-line and the article dims slightly — never a spinner in place of text, because there is already a complete answer on screen.
When it lands
The AI headline and body replace the computed ones in place and the by-line flips to Written by Neens AI. Nothing else on the page moves.
Changing the time range or the scope picker cancels everything in flight. A response for the window you just left is discarded rather than painted, so you never see last week’s briefing above this week’s numbers.
Telling an AI briefing from a computed one
The by-line under the article says so explicitly — different icon and different words:
| By-line | Icon | Meaning |
|---|---|---|
Written by Neens AI · <model> | sparkle | A model wrote this text, and that is the model that wrote it |
| Written by Neens AI | sparkle | A model wrote it; the model id wasn’t recorded |
| Auto-generated summary | calculator | Computed from the numbers. No model was involved |
Two more qualifiers can appear beside it: Updated <when> (when the text was generated) and cached (the text was served from storage rather than composed during this request — including a computed briefing that was stored earlier).
Why a briefing you’d disagree with is never shown
The briefing is stored with a fingerprint of the signals it describes — the fleet KPIs plus the identity of the top anomalies, issues and remediations. On every page load Neens recomputes that fingerprint and compares.
If it doesn’t match, or the stored text is older than the freshness window (10 minutes by default), the AI text is not shown. You get the computed briefing instead, and a fresh narrative is requested in the background.
This is deliberate. A narrative that says “error rate is holding steady” directly above a tile reading 22% is worse than a plain sentence of arithmetic: it teaches you to distrust the page. So a briefing that no longer matches its numbers is downgraded, never displayed.
This means an AI briefing can disappear and come back as a computed one — after Refresh, after new traces land, or after a detector pass changes the top issue. That is the guard working, not a fault.
Driving the briefing over the API
Three routes. GET /insights/summary is the one you read; the other two exist to ask for the AI narrative and to watch it being written.
Read the briefing
curl -G https://your-neens/api/insights/summary \
-H "Authorization: Bearer nk_live_…" \
--data-urlencode "range=24h"This route never calls a model and never queues work, so it is safe to call on every page load.
{
"headline": "12,480 traces · 4.1% errors · 3 open issues",
"body": "The fleet ran 12,480 traces at a 4.1% error rate, p99 latency of 3,180 ms and $41.62 in spend. Top anomaly: error rate moved from 0.021 to 0.058 on Support Agent. The leading issue is \"Unsupported order-status claims\" (214 affected).",
"llm": false,
"model": null,
"empty": false,
"stats": {
"sessions": 12480,
"errorRate": 0.041,
"p99Ms": 3180,
"spendUsd": 41.62,
"anomalyCount": 1,
"issueCount": 3,
"remediationCount": 2
},
"cached": false,
"generatedAt": "2026-07-30T08:14:02.418Z",
"briefing": {
"id": null,
"status": "missing",
"refreshing": false,
"source": "deterministic",
"signalDigest": "9c41ab77e2d0f3b18a4e5d62",
"error": null
}
}headline/body are always populated — unless empty is true, meaning there is nothing in the window to report, in which case they are null, the UI drops the article (the KPI tiles still render), and briefing.status is unavailable. llm is literal: true only when a model wrote the text you were just handed. The briefing object tells you what to do next.
Ask for an AI narrative
Only when briefing.status is missing or stale. Send the same window and scope you read the summary with — the briefing is identified by them, and a different window claims a different briefing.
curl -X POST https://your-neens/api/insights/summary/briefing \
-H "Authorization: Bearer nk_live_…" \
-H "Content-Type: application/json" \
-d '{"range": "24h", "from": null, "to": null, "projectIds": null}'The call is idempotent and single-flighted: however many clients ask for the same window and the same signals at the same moment, exactly one generation is started, so a hundred open dashboards cost one LLM call, not a hundred.
{
"briefing": {
"id": "brf_4b1e77c0a9d2f5386e0c14ab",
"status": "queued",
"refreshing": true,
"source": "deterministic",
"signalDigest": "9c41ab77e2d0f3b18a4e5d62",
"error": null,
"headline": null,
"body": null,
"model": null,
"generatedAt": null,
"stats": null
}
}Fields: range (default 24h), from/to (only meaningful with range: "custom"), projectIds (omit or null for your whole accessible scope).
Poll until it’s written
curl https://your-neens/api/insights/summary/briefing/brf_4b1e77c0a9d2f5386e0c14ab \
-H "Authorization: Bearer nk_live_…"This is a single-row read — no aggregation behind it — so polling it every few seconds is cheap. The in-app page polls every 3 seconds and gives up after 40 tries (about two minutes), keeping the computed briefing if it runs out.
{
"briefing": {
"id": "brf_4b1e77c0a9d2f5386e0c14ab",
"status": "ready",
"refreshing": false,
"source": "llm",
"signalDigest": "9c41ab77e2d0f3b18a4e5d62",
"error": null,
"headline": "Order-status errors double on Support Agent overnight",
"body": "Error rate on Support Agent jumped from 2.1% to 5.8% across 12,480 traces, driven by 214 sessions making unsupported order-status claims. p99 latency held at 3.2 s and spend was $41.62. Two remediations are already waiting for review.",
"model": "claude-sonnet-4-5",
"generatedAt": "2026-07-30T08:14:09.902Z",
"stats": {
"sessions": 12480,
"errorRate": 0.041,
"p99Ms": 3180,
"spendUsd": 41.62,
"anomalyCount": 1,
"issueCount": 3,
"remediationCount": 2
}
}
}stats is the snapshot of the numbers the narrative was actually written from, so you can render the text and the figures together and know they agree.
A briefing id is scoped like everything else. If your key or membership cannot see every agent the briefing covers, GET /insights/summary/briefing/{id} returns 404 — including for a briefing written over the whole workspace when you only have some agents.
Briefing statuses
briefing.status is the whole client contract. There are seven values:
status | Meaning | What a client should do |
|---|---|---|
ready | A stored briefing matches the current numbers and is inside the freshness window, and it is what you were served. Check source — it is llm for an AI narrative and deterministic for a stored computed one | Nothing. Render it |
queued | A generation has been claimed and is waiting for a worker | Poll GET /insights/summary/briefing/{id}. Do not POST again |
running | A worker is writing it now | Poll. Do not POST again |
stale | An AI briefing exists but describes signals that have moved on, or has aged past the freshness window | POST once to request a fresh one, then poll |
missing | Nothing has ever been written for this window and scope | POST once to request one, then poll |
failed | Generation was attempted and failed. error carries a short reason | Show the computed briefing. Retry later or after Refresh; do not hammer it |
unavailable | No usable LLM connection for this scope, or there is nothing to report | Do not poll and do not POST. The computed briefing is the final answer |
unavailable is terminal by design, not a transient condition to retry through. A client that polls on unavailable will poll forever against an agent that will never have a narrative. Treat ready, failed and unavailable alike as stop.
Two extra fields matter. refreshing is true when a newer narrative is being written behind the text you were just served — that’s the cue for the small “writing a fuller briefing…” note; keep showing what you have and poll. And source (llm or deterministic) is the only honest way to know who wrote the text you are holding: do not infer it from status, because a computed briefing can be stored and served exactly like an AI one.
A stored computed briefing that still matches the numbers is served as-is rather than being re-written — a fresh, correct briefing is not withheld on the grounds that a model might phrase it better. So right after you add an LLM connection to an agent that already has one stored, the by-line can stay Auto-generated summary until the freshness window lapses (10 minutes by default) or the signals move — Refresh re-runs the detectors, which usually does move them. Over the API you can force the question immediately: a stored computed briefing is deliberately not treated as a hit by POST /insights/summary/briefing, so that call starts a real generation.
When generation fails
A failed briefing always carries a short, fixed reason code in error — never a raw error message. The computed briefing stays on the page throughout.
error | What happened | What to check |
|---|---|---|
llm_error | The provider rejected the call or errored | The connection under Settings → Connections: credential still valid, model id still offered by the provider, provider not rate-limiting you |
timeout | The provider didn’t answer within 30 s | Provider status; a very slow self-hosted endpoint. Egress from the Neens host if you self-host |
unusable_output | The model answered, but not with a usable headline and body | The model on that connection — very small or heavily quantized models often can’t hold the requested JSON shape. Try a stronger model |
max_attempts | The same briefing failed the retry budget (3 attempts by default) for the same story | The underlying cause of the earlier failures. The budget resets by itself as soon as the signals change, so this never means “never again” |
empty_scope | By the time the worker ran, the window had nothing in it to narrate | Usually benign — a scope whose traces aged out of the window, or an agent that stopped sending. Confirm ingest is still arriving |
internal_error | An unexpected server-side error | The server log for that time window; if self-hosting, the worker’s log |
Note that “this agent has no LLM connection” is not in that list. It is not a failure — see below.
No LLM connection is a supported end state
Neens holds no platform API key. Every LLM feature — the briefing included — runs on the connection you configure under Settings → Connections, on your own account and your own spend.
An agent with no connection gets the computed briefing, permanently. briefing.status reports unavailable, no generation is ever queued, nothing is retried, and nothing is logged as broken. The page is complete: real headline, real body, real numbers, honest by-line. Adding a connection later is all it takes for the AI narrative to start appearing — there is nothing else to switch on.
Lifecycle: dedup, recurrence, resolution
Every insight carries a stable identity (agent + signal, never the run), so re-detection updates one row instead of stacking duplicates:
- First detection ⇒ new. Detected again after being resolved ⇒ recurred. An acknowledged insight stays acknowledged while its signal persists.
- When a detector-owned signal stops firing, the insight is automatically marked resolved on the next pass — feeds only show open insights.
- Feedback survives re-detection because it’s keyed to the same identity.
Feedback and muting
Each insight accepts a verdict — useful, actioned, not_useful, wrong, or dismissed. A negative verdict (not useful / wrong / dismissed) mutes the insight: it disappears from the feeds and stops notifying, until the signal fully resolves and then recurs fresh — at which point it surfaces again as a genuinely new occurrence. Positive feedback is kept for ranking and audit and never suppresses anything.
Persona-aware routing
Detectors stamp every insight with an intended audience (engineer, PM, exec, support) and routing tags (reliability, latency, cost, quality, compliance, safety, pii, issue). Regressions on safety-flavored metrics (toxicity, PII leakage, bias, hallucination, …) automatically pick up compliance/safety tags.
Persona lenses use these to decide which insights lead for a user: an Executive lens leads with reliability/quality/cost signals, Finance with cost, Compliance with compliance/safety/PII signals and pre-prod regressions, Product manager with quality regressions. Catch-all lenses (developer, admin, full workspace) see everything. Routing shapes the digest email below — the in-app feeds always show the full picture, and a lens never hides data you have access to.
Notification sinks
When an insight newly surfaces (first appearance, or recurrence after resolve/mute), it’s dispatched to the configured sinks:
| Sink | Default | What it delivers |
|---|---|---|
| In-app | Always on | The Insights page itself — the store is the feed |
| Slack | Off | High-severity insights only, posted to a Slack incoming webhook as a short summary |
| Webhook | Off | The full structured insight JSON (agent, evidence, impact, recommendation, tags) POSTed to any URL — the hook for GRC/ticketing/automation workflows |
Delivery is best-effort: a down Slack or webhook endpoint is logged and never breaks detection.
Want to alert on a specific number you choose — “page me when eval pass-rate < 90%” — rather than the detectors’ automatic signals? Define an alert rule. Alert rules fire through these same sinks.
The digest email
The digest is an opt-in, per-user email — “your fleet, every morning”:
- Opt in under Settings › Account (or
PUT /auth/digest), choosing daily or weekly. It’s off by default, and capped at one email per day per user. - Contents: the fleet-health tiles — Traces, Error rate, Eval pass rate, Spend — computed through the same metrics catalogue the dashboards use, plus up to five open “needs attention” insights routed to your persona lens, each deep-linking into the app. When your agent has committed to Business KPIs, the digest adds a Business KPIs section — your top few by priority, each with its value, target status (met / missed / unknown) and trend. A KPI with no number yet shows —, never a zero.
- Scope-safe: every number and insight is limited to the agents you could see in the app.
- Send time: the daily job fires at 13:00 UTC. Weekly recipients get theirs on the same tick once a week.
- Never noisy: a digest with nothing to say (no activity, no insights) is skipped, not sent empty.
- Try it: Send me a test digest in Settings (or
POST /auth/digest/test) composes and sends yours immediately, ignoring the daily cap.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Feeds are empty right after ingesting | Detectors run nightly | Use Refresh to run them now |
| A known problem never appears | It’s below the documented floors (e.g. fewer than 5 traces, or a <5-point drop) | The thresholds guard against noise; check the raw data via Failure clustering |
| A dismissed insight came back | Its signal resolved and then recurred fresh | That’s by design — a genuinely new occurrence resurfaces; dismiss it again if it’s still noise |
| No digest arrives | Not opted in, or nothing to report | Opt in under Settings › Account; a digest with no activity or insights is skipped rather than sent |
| Slack is silent | Nothing high-severity surfaced | The Slack sink only posts high-severity, newly surfaced insights |
| The briefing always says Auto-generated summary | No LLM connection for that agent | Configure a connection under Settings → Connections. An agent with no connection keeps the computed briefing — a supported end state, not a fault |
| The AI briefing keeps reverting to the computed one | The signals changed, so the stored narrative no longer matches the numbers next to it | Expected. A fresh narrative is requested automatically; the computed text holds the page until it lands |
| ”Writing a fuller briefing…” never finishes | On a Celery deployment, the eval worker fleet isn’t running | Check the nq.scorers.* consumers. If judge and eval runs are also stuck, fix the worker |
| A short reason appears next to the by-line | Generation failed | Look the code up in When generation fails — each one names what to check |
Related
- Dashboards — build the charts these signals point you at.
- Metrics catalogue — the measures behind the fleet stats and digest tiles.
- Failure clustering and Remediations — where insight links land.