Insight config reference
The shape of the config object on POST /builder/insights — an explore spec under config.query (trend, funnel, or attribution for ad spend, CPA and ROAS) or an analysis panel under config.analysis (constraint finder, correlation, scanner summary) — with a working example per insight_type, the type rules, and how to read the result.
Overview
An insight is a saved explore query or an analysis panel plus a visualization. Its config is a JSON object with exactly one of query or analysis:
{
"name": "Daily visitors",
"insight_type": "line",
"config": {
"query": {
"kind": "trend",
"date_range": { "preset": "30d" },
"interval": "day",
"series": [{ "event": "$pageview", "math": "dau", "label": "Visitors" }],
"compare": "previous_period"
}
}
}
config.queryis the same object you wouldPOST /client/v1/data/query. The full grammar — series maths, filters, breakdowns, funnels, limits — lives on the Explore queries page and is not repeated here.insight_typeis only the visualization. It never changes what is computed.config.analysisreplacesqueryfor a constraint finder, correlation or scanner summary panel. See Analysis panels.- Any other key in
configis stored verbatim as display settings and never read by the runner, exceptmetricon an attributionnumber(see Ad spend, CPA and ROAS).
config is validated on write. POST /builder/insights returns 400 { "error": "invalid config", "details": [...] } for a missing or invalid query or analysis, both at once, or a kind that doesn't fit the insight_type. Nothing invalid reaches the table, so a saved insight cannot fail to compile later.
Which spec per type
insight_type | query.kind | Typical spec | Renders |
|---|---|---|---|
number | trend | interval, 1–5 series, optional compare | a scorecard per series (total + delta vs previous period) over one shared trend chart |
line | trend | interval, 1–5 series, optional breakdown | one line per series × breakdown value |
bar | trend | no interval, breakdown or several series | one bar per breakdown value or series |
pie | trend | no interval, breakdown | share per breakdown value |
table | trend | with or without interval | series × buckets, or one row per series |
funnel | funnel | 2–10 steps, optional window, grain, breakdown | step bars with conversion rates |
table, bar, number | attribution | goal or deals, level, model | spend, clicks, conversions, CPA and ROAS per campaign, ad set or ad |
The hard rules: funnel ⇔ kind: "funnel", and attribution only draws as table, bar or number. Every other type carries a kind: "trend" spec. paths cannot be saved. A trend without interval returns one value per series; with interval it returns data[] per bucket and a total. bar and pie read the totals either way, so leaving interval off is cheaper for those. number renders either way too, but without an interval it has no buckets to draw and shows the scorecards alone.
Examples
number — conversions this week vs last
{
"insight_type": "number",
"config": {
"query": {
"kind": "trend",
"date_range": { "preset": "7d" },
"series": [{ "event": "purchase", "math": "total", "label": "Purchases" }],
"compare": "previous_period"
}
}
}
line — visitors by device
{
"insight_type": "line",
"config": {
"query": {
"kind": "trend",
"date_range": { "preset": "30d" },
"interval": "day",
"series": [{ "event": "$pageview", "math": "dau" }],
"breakdown": { "property": "$device_type", "limit": 3 }
}
}
}
pie — traffic sources
{
"insight_type": "pie",
"config": {
"query": {
"kind": "trend",
"date_range": { "preset": "30d" },
"series": [{ "event": "$pageview", "math": "unique_sessions" }],
"breakdown": { "property": "$referring_domain", "limit": 8 }
}
}
}
table — top pages
{
"insight_type": "table",
"config": {
"query": {
"kind": "trend",
"date_range": { "preset": "7d" },
"series": [
{ "event": "$pageview", "math": "total", "label": "Views" },
{ "event": "$pageview", "math": "dau", "label": "Visitors" }
],
"breakdown": { "property": "$pathname", "limit": 20 }
}
}
}
funnel — checkout
{
"insight_type": "funnel",
"config": {
"query": {
"kind": "funnel",
"date_range": { "preset": "30d" },
"steps": [
{ "event": "$pageview", "filters": [{ "property": "$pathname", "op": "eq", "value": "/pricing" }], "label": "Pricing" },
{ "event": "checkout_started" },
{ "event": "purchase" }
],
"window": { "value": 3, "unit": "day" }
}
}
}
Set "grain": "person" on a funnel whose steps cross from the browser into
server-side or CRM events — those arrive under a contact id, not the visitor's
distinct_id, so the default device grain cannot link them. See
Explore queries for the cost.
Ad spend, CPA and ROAS
An attribution spec credits each conversion to the Meta or Google campaign, ad set or ad that brought the visitor, and joins the platform's daily spend. The result has totals (all conversions), filtered and one row per campaign/ad set/ad (and per non-ad channel) with spend, impressions, clicks, conversions, revenue, cpa and roas. cpa is null when no spend was recorded, never 0. See Explore queries for the grammar.
{
"name": "Cost per lead by ad",
"insight_type": "table",
"config": {
"query": {
"kind": "attribution",
"date_range": { "preset": "30d" },
"goal": { "event": "lead_submitted" },
"level": "ad"
}
}
}
A number shows one total, chosen by config.metric: conversions (default), revenue, spend, roas or cpa. A bar draws the same metric per row.
{ "insight_type": "number", "config": { "metric": "cpa", "query": { "kind": "attribution", "date_range": { "preset": "30d" }, "goal": { "event": "lead_submitted" } } } }
Analysis panels
config.analysis is { "kind", "params" }. The panel runs an existing analysis route as the viewer: their key is sent to the route, so the route's own scope, plan and client checks apply, and the result is cached for 5 minutes per viewer key. Analysis panels therefore do not run on public or embedded dashboards (POST /public/query answers 403).
kind | Route it runs | insight_type | params |
|---|---|---|---|
constraints | GET /client/v1/constraints | table (rows and their constraint), number (recoverable conversions at the primary constraint), bar (recoverable conversions per step) | conversion_event, lead_event, group_by (campaign, adset, ad), days or from/to, timezone, attribution, min_impressions, campaign, account_id, attribution_window_days, resolve_identities, revenue_path, benchmark_ad/tracking/landing/sales/site (0-1) |
correlation | GET /client/v1/correlation | line (each metric per bucket), table (pairwise Pearson and Spearman) | metrics (array of 2-5 ids, required), grain, lookback_days or from/to, rolling_window, max_lag, new_lookback_days, revenue_path |
scanner_summary | GET /client/v1/replay-vision/scanners/{id}/summary | number (headline), pie/bar/table (answer counts), line (answers per day) | scanner_id (required), days or date_from/date_to |
Parameters are typed (numbers as numbers) and allowlisted: the constraint finder's table and include are not available to panels. POST /insights/{id}/query and run_insight return { "engine": "analysis", "kind", "result" } where result is the route's response body (named result so it is not mistaken for the response envelope's data).
{
"name": "Where the paid funnel leaks, per ad",
"insight_type": "table",
"config": { "analysis": { "kind": "constraints", "params": { "conversion_event": "lead_submitted", "group_by": "ad", "days": 30, "timezone": "America/Los_Angeles" } } }
}
{
"name": "Sessions with a broken experience",
"insight_type": "number",
"config": { "analysis": { "kind": "scanner_summary", "params": { "scanner_id": "921f8759-aafa-4595-ad62-b70d00354b68" } } }
}
Discovering events and properties
Don't hardcode names. The Data API answers, with the same key:
GET /client/v1/data/events/summary— event names with counts for a window.GET /client/v1/data/properties?event=<name>— the property catalog (typed keys and how often they occur).GET /client/v1/data/query/values?property=<key>&event=<name>&search=— top values of one property, for filter and breakdown pickers.
Validating and previewing
Validate on every edit
POST /insights/validate runs the write-path rules with no query: { "ok": true } or 400 { "ok": false, "errors": [...] }.
Preview a draft
POST /insights/preview runs { insight_type, config, dateRange?, granularity? } in memory and returns the same payload as the saved runner. Nothing is persisted; identical drafts are served from a 120-second cache.
Save
POST /insights stores it. Change the config later with the same call plus id; change name, description, tags or favorite with PATCH /insights/{id}.
Per-run overrides
POST /insights/{id}/query, POST /public/query and /insights/preview accept two optional overrides that are applied onto the stored spec for that run only:
dateRange—{ "preset": "7d" }(24h,7d,14d,30d,90d,365d) or{ "preset": "custom", "from": "2026-08-01", "to": "2026-08-31" }. Anything else keeps the stored range.granularity—hour,day,week,month; replacesquery.interval. Trends only.
On an analysis panel, a preset becomes the route's relative window (days, or lookback_days for correlation, which ignores 24h) and a custom range its dates (from/to, or date_from/date_to for a scanner summary).
Dashboards use these to apply one date range and interval to every tile.
Reading the results
The run payload is the explore response with the insight attached — see the Queries group for the full schema. In short:
| Field | Trend | Funnel |
|---|---|---|
kind | "trend" | "funnel" |
buckets / previous_buckets | ISO labels, present with interval | — |
series[] | { label, math, event, breakdown_value?, data[] | value, previous[] | previous_value, total } | — |
breakdown_values[] | values in rank order, $$other last | — |
steps[] | — | { step, label, event, users, conversion_from_first, conversion_from_previous } |
breakdown[] | — | { value, steps[] } per breakdown value |
meta | resolved window, interval, breakdown settings, sql_ms, rows_read | same, plus grain |
config, cached, sql | stored config, cache hit flag, compiled query (sql is omitted on /public/query) | same |
For a number, read each entry's total (with interval) or value for the scorecard, the previous* counterpart for the delta, and buckets against data for the chart beneath them. For a line, plot buckets against each entry's data. For bar/pie, one slice per series[] entry using total/value.
Dashboards: filters and theme
POST /builder/dashboards takes filters and theme objects, both {} by default.
Default date range and granularity applied to every tile on load. Shape: { "dateRange": { "preset": "7d" }, "granularity": "day" } (the per-run overrides above). Embed viewers can override via ?date_range=/?granularity= query params.
Reserved; currently unused — leave it {}. Embed theming is driven by query params on the embed URL (theme, theme_primary, theme_secondary, theme_heading_font, theme_body_font, transparent, hideHeader, branding), not by this stored object.