Skip to content

Data & analytics

BI Workspace

Dashboards built on the same catalog, prepared tables and metrics your agents use — so a chart and an agent answering about it cannot disagree.

Open Data & BI → BI Workspace. Dashboards live in workspaces and folders; each is a grid of resizable widgets you drag into place.

AI Analyst — conversational analysis

AI Analyst (first under Data & BI) is the dedicated ask-anything surface. An analyst is exactly two choices: a reasoning model from your connected providers (the dialog suggests reasoning families and nudges you when a pick doesn't look like one) and the data it analyses — local datasets and uploads, one table, or a warehouse connection. Both stay editable from the pencil on the analyst's card, and an edit applies to your next question: earlier analyses are not re-run and each keeps a record of the model that produced it, so an exported report never attributes old numbers to a newly-chosen model. Reasoning models are also given a longer request deadline than chat models, because their time goes on thinking rather than output — sized for chat models, the analyst's required model class was the one that timed out.

Governed steps are compiled, not described. When a step's numbers come from a governed semantic model, the plan names the model, its metrics and its dimensions, and the semantic compiler writes the SQL — with the fan-out refusals, rollup routing, row filters and column masks a hand-written SELECT cannot honour. Every name is checked against the catalog, and a block naming anything the model does not have is dropped whole: compiling the half that matched would answer a different question under a governance badge. A dropped block is not a refusal — the step falls back to written SQL and appears without the badge. A step whose SQL is later corrected by the self-check loses the badge too, because the correction is the analyst's SQL, not the compiler's. Where a rollup answered instead of the fact table, or a row filter narrowed the result, the step says so in words. Past analyses are all reachable from the picker beside New analysis.

What-if scenarios ride the same compiler. A compiled step offers the two things that can honestly vary — the model's declared parameters and the values of filters the step already has — and recompiles the same query with one thing changed, so the difference between the numbers is that change and nothing else. The result is labelled with exactly what was assumed, shown beside the measured result rather than replacing it, and never folded into the findings. Where both sides are a single row the delta is computed in code; where they are grouped the comparison is refused rather than matching rows by position. A scenario that varies nothing is refused too — re-running an identical query under a “scenario” heading invites the reader to conclude a change was tested and made no difference.

Verified answers. Mark a finished analysis verified, or flag it wrong with a reason — a flag without one leaves the next reader where they started. The verdict records who and when, and is pinned to a fingerprint of the steps it judged: each step's SQL and the governed model that compiled it. Edit a step, or let the self-check rewrite one, and the verdict voids — shown as void rather than dropped, because the reader needs to know a check existed and no longer covers these queries. Results and prose sit outside the fingerprint: the same SQL over refreshed data is the same checked work. Ask a question someone has already judged and the verdict is offered, not applied — asking re-runs the queries against today's data, and only still-valid verdicts are offered. Every question runs a transparent loop: plan the steps, write and run the SQL (the same generator and governed metric definitions the BI analyst uses, SELECT-only), check its own work — a wrong step re-runs with corrected SQL, a doubtful one is presented flagged — then write up findings where every number cites the step it came from. Analyses persist per analyst, follow-up questions see the earlier answers, and the whole trace exports as a PDF.

Every step that has something to show gets its own chart, so a three-query answer arrives as three visuals rather than one headline picture. Any step can be pinned to a dashboard — the widget keeps that step's SQL and chart, and re-runs it on refresh — or edited and re-run with your own SQL. When you edit one, the findings above are marked written before a step was re-run until you rewrite them, and the step drops the check verdict that judged the previous query: a green “Check passed” must never vouch for SQL the analyst never saw. After each answer it offers follow-up questions, and a brand-new analyst offers starter questions written from its own schema.

Three kinds of reasoning are computed in code, not narrated, because a plausible number reads exactly as confident as a correct one. A step returning two periods side by side gets contribution analysis — each member's change and its share of the total change, with drivers separated from the offsets a headline hides, and shares left above 100% when winners and losers cancel. A time-series step gets its trend and outliers (median-based, so the spike does not hide inside the statistic judging it) and, with at least eight periods, a projection labelled as an estimate carrying its method and fit error — with less history it refuses to extrapolate at all. And when a question genuinely cannot be answered without knowing something — an unstated time range, a word your schema defines two ways — it asks before querying, offering the assumption it would otherwise make so accepting takes one click.

Steps run concurrently — a three-query analysis issues its queries at once, bounded so one question cannot become everyone's rate limit. That is safe because no step consumes another's output: each writes its SQL from its own goal, and results are read only once every step has finished. Results are matched back to steps by index, never by completion order, so the trace still reads top to bottom; you just see steps finish out of order. Identical SQL inside one analysis runs once — the cache holds the in-flight query, so two steps asking the same thing at the same moment share one round-trip. It is never carried across questions, because between two questions the data can move and a cached row served later is a number that is no longer true.

Schedule re-runs an analysis hourly, daily or weekly — and it re-runs the saved queries, it does not ask the question again. Re-planning would let the SQL differ between runs, so the number you watch over time would have a definition that moves underneath you. The steps are pinned instead, no model is called, and the findings are marked written before a step was re-run until you rewrite them. A human verdict survives (the SQL it judged did not change); a what-if is dropped, because one computed against last week is not a what-if against this week. The digest reports what moved — precisely for a single-row result, as a row count where results are grouped, since matching rows between runs would invent findings — and says so plainly when nothing changed. Run now takes the identical path.

Sharing an analyst shares the analyst, not your data access. Grant it to IAM groups and recipients can open it and ask their own questions — but every query they run is authorised as them, with their dataset grants, their warehouse credentials and their row filters. A shared analyst can therefore return different numbers to different people, which the share dialog states before you grant, blocking problems first. An analyst scoped to all local datasets resolves per reader, so shared it points at the recipient's data rather than yours. Your saved analyses stay yours — they hold rows fetched under your access — and renaming, editing and deleting remain owner-only. A grant is refused outright if the recipients' IAM model rules do not allow the analyst's model.

Every step that ran a query carries a Where these numbers came from disclosure. The tables it names come from the SQL that actually ran, never from the model definition — models get edited, and a panel built from today's definition would misdescribe a query that ran against yesterday's. So a step answered by a rollup names the rollup and says the fact table was not read, and a step whose SQL you edited says no governed definition vouches for it. Under each table, where the evidence exists, sits what a prep flow combined to build it and what the warehouse's own lineage records upstream — loaded only when you open the disclosure. A lookup that fails says so, rather than reporting “nothing upstream”.

Export data gives you the analysis as a workbook rather than a document: an overview sheet with the questions, findings and — per step — its governed model, check verdict, human verdict and whether it was edited by hand, then one sheet per step result. A spreadsheet gets mailed around and outlives the query behind it, so the qualifiers travel in the cells: the file is stamped with its export date, and what-if rows get their own sheet with the assumption in the first column — an unlabelled hypothetical becomes a measurement the moment someone copies it. Steps that returned no rows are skipped rather than exported as empty sheets.

Building a dashboard

  1. 1

    Create a dashboard

    Give it a home folder — folders are how you keep dev and production content apart.
  2. 2

    Add a visual

    The right-hand builder pane takes your source (table, prepared table, or a governed metric), the fields to plot, and the chart type.
  3. 3

    Or describe it

    The AI tab writes a whole dashboard, or a single visual, from a sentence. Point it at a table and it writes SQL you should read before trusting the chart; point it at a semantic model and the compiler writes the SQL instead.
  4. 4

    Arrange and publish

    Drag and resize, then publish to make it visible to the people you've shared it with.

You can also send a chart straight from the SQL workbench with Add to dashboard once a query returns something worth keeping.

Generate a dashboard from a governed model

Generate dashboard with AI offers two sources: your tables, or a semantic model. Choosing a model governs the whole dashboard.

On the governed path the planner is shown the model's declared metrics and dimensions — deliberately not the physical tables or columns, because showing it the schema invites it to reach past the layer this path exists to enforce. It picks names from that vocabulary and the compiler turns each choice into SQL. So the worst a bad suggestion can do is ask a well-formed question nobody wanted, rather than compute a number nobody defined.

Everything it returns is checked before it reaches the compiler: an undeclared metric or dimension, a time grain on something that is not a time, a rollup the compiler does not know, a chart shape that cannot render what it was given, or a duplicate query wearing a second name. Each is refused with a reason naming the offending item, and the rejections are shown in the dialog rather than swallowed — a generate that proposed twelve and built nine has to say which three it lost.

Metrics only, chosen once

The source is picked at the dialog, not per widget. Mixing governed and ungoverned visuals would let an ungoverned chart hide among certified ones on a dashboard whose whole claim is that its numbers are defined. Questions the semantic model cannot express are simply not asked.

How a widget gets its data

Every chart widget stores the SQL it ran and a snapshot of the rows that came back. Which of those a viewer sees depends on the widget's data mode, and the difference decides freshness, cost, and — in one case worth reading carefully — whether the number is right.

ModeWhat happens at view timeUse it when
Import (default)The stored snapshot renders. No query runs, nothing touches your warehouse, load is instant.Almost always. Pair it with a refresh schedule.
DirectThe widget's SQL re-runs against the warehouse on every view, for the current truth.A number that must be live, on a dashboard few people open.

Published and embedded dashboards always render the snapshot

Direct mode applies to the editor and to signed-in viewers. A public share or embed renders the stored snapshot whatever the widget's mode says — otherwise every anonymous visitor would be a live query against your warehouse. So on a published dashboard, the refresh schedule is the freshness, and each widget shows when it was last computed.

The row cap, and the totals it can quietly break

A snapshot holds at most 500 rows. That is fine for a chart of twelve months or twenty regions, and it is a problem for a chart built over raw rows — because the browser is what sums duplicate categories together.

A capped snapshot sums a subset and calls it the total

Chart sum(amount) by region over a 50,000-row table and the snapshot keeps the first 500. The renderer then adds up the regions it can see, and draws a bar chart that looks entirely normal and is wrong — no error, just a smaller number. Widgets in this state carry a Partial badge, and the fix is the next section.

Aggregate in SQL

Turning on Aggregate in SQL compiles the chart into a GROUP BY and makes the database do the adding. One row comes back per category instead of thousands of raw rows, so the totals are complete and the cap stops mattering:

sql
-- what the widget stores
SELECT region, amount FROM sales

-- what actually runs with "Aggregate in SQL" on
SELECT region, SUM(amount) AS amount
FROM (SELECT region, amount FROM sales) AS _dq
GROUP BY region

Supported aggregates are sum (the default), avg, min, max, count and count_distinct. Wrapping SQL that already aggregates is harmless — grouping rows whose key is already unique returns them unchanged.

Why it isn't just switched on everywhere

New widgets get it by default. Existing ones deliberately do not, because switching it on can change a number someone is already reporting — the old number was a partial sum, and correcting it silently would be its own kind of dishonesty. They surface the Partial badge instead, so the owner sees the problem and makes the change themselves.

Aggregating in SQL fixes the values, not the row count

Pushdown makes each row’s number complete. It does not shorten the result: a daily series over a year still returns 365 rows, and if that exceeds the snapshot cap the tail of the chart is simply absent. A cumulative line measured this way ended its year twelve times short while every visible point was individually correct. So the Partial badge appears whenever a snapshot hit the cap — including aggregated widgets, where it means “rows are missing from this chart” rather than “this total is a subset”. Narrow the query, or raise VITE_BI_SNAPSHOT_ROWS_CAP.

Incremental refresh

For a large table where only recent rows change, a widget can re-query just a recent window on a chosen date column and keep the previous snapshot's older rows. A refresh then reads a week instead of three years. Bind it in the builder: pick the date column, then Full re-query or a window of 7, 30, 90 or 365 days.

It assumes history does not change

Whole time buckets are recomputed rather than partial aggregates merged — which is what keeps avg and count_distinct correct, since those cannot be combined from partials. The trade you are accepting is the usual one: a late-arriving edit to a row outside the window will not appear until a full refresh. If your source back-dates corrections, either widen the window past your correction lag or leave incremental off.

Chart types — all 26, with required fields

Each chart declares which fields it needs. The builder only offers types your selected columns can satisfy, so a chart missing a required field cannot be created.

Comparison and trend

TypeRequired fieldsOptionalUse for
barxField, yFieldseriesField, stackedVertical columns; ranking and comparison
hbarxField, yFieldHorizontal bars; long category names
linexField, yFieldseriesFieldChange over time
areaxField, yFieldseriesFieldTrend with volume emphasis
scolumnxField, yField, seriesFieldStacked vertical columns
shbarxField, yField, seriesFieldStacked horizontal bars
comboxField, barField, lineFieldTwo measures at different scales (volume + rate)
waterfallxField, yFieldHow a total is built up from contributions

Part of a whole

TypeRequired fieldsUse for
pienameField, valueFieldFew slices — beyond ~6 it stops being readable
treemapnameField, valueFieldMany parts, nested by size
funnelnameField, valueFieldStage-to-stage drop-off
nightingalenameField, valueFieldPolar rose — cyclical categories
sankeyxField (source), yField (target), valueFieldFlow between nodes

Single value

TypeRequired fieldsOptionalUse for
kpivalueFieldlabel, targetFieldOne headline number, optionally against a target
gaugevalueFieldlabel, targetField, maxProgress toward a ceiling

Distribution and relationship

TypeRequired fieldsOptionalUse for
scatterxField, yFieldsizeFieldCorrelation between two measures
heatmapxField, yField, valueFieldDensity across two dimensions
boxplotxField, yFieldSpread and outliers per category
radarxField, yFieldseriesFieldSeveral measures compared across entities

Tables and geography

TypeRequired fieldsOptionalUse for
tablecolumnFormatsExact figures read row by row
matrixrowField, colField, valueFieldrowSubField, condFormatPivot. rowSubField makes rows expandable groups with subtotals; condFormat colours cells.
maplocationField, valueFieldChoropleth by region
bubblemaplocationField, valueFieldMagnitude at points

Specialised

TypeRequired fieldsOptionalUse for
barracexField, yField, timeFieldAnimated ranking over time
wordcloudtextFieldvalueFieldTerm frequency; weight by valueField when you have one
ontologyspecAI-built knowledge graph: subject–predicate–object triples across datasets, warehouses and KB knowledge graphs; nodes and edges are clickable

Why it works this way

Pick the chart from the question, not the other way round. "Which region is biggest" is a bar; "is it growing" is a line; "what share" is a pie with few slices. If you cannot state the question a visual answers in one sentence, it is decoration — and decoration is what makes a dashboard stop being read.

Two widgets that plot nothing

Not everything on a dashboard is a chart. Two widget types take no data at all, and they are what turns a wall of numbers into something a reader can follow without being told what it means.

WidgetTakesUse it for
textA title and Markdown content — no fields, no query.The executive summary at the top, a definition of a metric people keep misreading, a note on why last month looks odd.
imageAn uploaded image or an image URL.A logo, an architecture diagram, a photograph — anything the numbers are about but cannot show.

A dashboard nobody can read is a dashboard nobody uses

These two cost nothing to add and are the difference between a page that answers a question and a page that presents evidence and leaves the reader to infer the question. A text widget stating what the dashboard is for, and one sentence per section saying what good looks like, does more for adoption than another chart.

Number formatting

OptionValuesNotes
formatcurrency | percentOmit for a plain number
currencyISO 4217 codeDefaults to USD when format is currency
decimals0 – 4Fixed fraction digits. Leave undefined for automatic/compact (1.24M).
columnFormatsper columnTable widgets only — format each column independently (number | currency | percent, with its own currency and decimals).

Analytics options

Available on every chart spec; each renderer applies the ones it supports. This is where most of the analytical value lives, and it is the part people miss.

OptionValuesApplies toEffect
drillFieldsstring[]bar, hbar, pieDrill hierarchy; level 0 is the configured field. Readers descend one level per click.
dateGrainauto | day | week | month | quarter | yearline, areaDefault bucketing; viewers can toggle it.
compareprior_period | prior_yearline, area (single series)Overlays the previous bucket or the same bucket last year.
runningbooleanline, area (single series)Cumulative running total.
trendbooleanline (single series)Linear trend line.
forecastnumber of bucketsline (single series)Projects ahead with a ±1.96σ confidence corridor.

A forecast is a straight-line projection

The ±1.96σ corridor is a 95% band around a linear extrapolation, not a model of your business. It is honest about uncertainty and blind to seasonality, launches and price changes. Use it to frame a conversation, never to commit to a number.

Conditional formatting (matrix)

ModeConfiguration
scaleContinuous colour scale across the values, with an optional base colour.
rulesAn ordered list — first match wins. Each rule is an operator (gt, gte, lt, lte, eq, neq, between), a value (plus value2 for between, inclusive) and a colour.

Reference lines

A horizontal reference line on cartesian charts, in one of two modes: avg draws the series average, or value draws a fixed number you supply. Both take an optional label — use it, because an unlabelled line invites the reader to guess what it means.

Making it interactive

Global filters
Dashboard-level controls — value pickers, numeric ranges and relative-date presets (last 7 days, this quarter). Save defaults so the dashboard opens on the right view.
Cross-filtering
Click a bar and every other widget filters to it. The fastest way to answer "what's driving that spike" without building anything.
Drill hierarchies
Define year → quarter → month, or region → country → city, and let readers descend a level at a time.
Drill-through
Open the underlying rows behind any data point — the answer to "is this number real?" and the fastest way to spot a broken join. Everything narrowing the view goes into the query: the widget's own filters, the drill level you clicked into, the active cross-filter. The 1,000-row cap applies after that, so the count is the real number of matching rows and the cap is disclosed when it bites. A category computed in the widget's SELECT list (DATE_TRUNC(…) AS month) has no column to trace back to, and that is refused rather than answered with the whole table.
Ask AI (embeds)
On an EMBEDDED dashboard, readers can ask a follow-up question of its data in natural language, including a drill-down on what they clicked. Inside the app this is not a separate control — the AI analyst in the builder answers the same questions with more of the model behind it.

AI insights

Each visual can generate a written reading of what it shows — the notable movement, the outlier, the thing a person would say out loud. There is also a dashboard-level digest summarising the whole page.

Careful

Insights are generated from the data actually in the chart, but they are still model output: useful as a first pass, not as a substitute for looking. Treat them as a colleague's first impression.

Scan — computed, not generated

Scan in the toolbar is the opposite kind of thing: it runs the obvious checks across every widget's snapshot using arithmetic, not a model. No call, no cost, no wait, and the same answer twice. It looks for three things and states the bar for each:

  • Trends — a least-squares slope of at least 2% per period, over at least five points.
  • Outliers — 3 or more MAD from the median. Median and MAD, not mean and standard deviation, because an outlier inflates the very standard deviation a naive check would judge it by.
  • Concentration — one member holding 60% or more of a total, across at least four members. (With two, one is always over half.)

Findings are ranked by how far each cleared its own threshold — shown as 2.6× — which is what makes an outlier and a trend comparable at all.

Finding nothing is not an all-clear

A scan that returns nothing says so precisely: how many widgets it examined, how many it could not and why, and the exact thresholds it applied. Widgets get skipped for real reasons — a snapshot that hit its row cap holds an arbitrary subset, so every total or share over it would be computed from part of the data; a share of a measure with negative values can exceed 100% or flip sign. In both cases the scan refuses and names the reason rather than printing a confident number.

Alerts and scheduled reports

  • Alerts — watch a metric against a threshold and notify when it crosses. Delivered in-app and by email.
  • Scheduled refresh — rebuild imported data on a cadence so the dashboard isn't stale.
  • Incremental refresh — re-query only a recent window instead of the whole table. Covered in how a widget gets its data, along with the assumption it commits you to.
  • Scheduled reports — email a digest of the dashboard on a schedule.

Aggregate in SQL — complete totals on any table size

A chart widget stores a capped snapshot (500 rows). On a large table, summing a capped snapshot in the browser is a partial total — the number looks confident and is quietly wrong. New widgets therefore aggregate in the database by default (a validated GROUP BY compiled from the chart), so the warehouse returns complete grouped rows instead of raw ones. Existing widgets are not switched automatically — turning it on can change the number they display, which is the owner's call — they show a Partial badge when their snapshot hit the cap, with an "Aggregate in SQL" toggle in the widget menu.

On a self-hosted deployment these need the scheduler running — see Install & deploy.

Sharing, export and embedding

MethodWho can see itNotes
Group shareNamed users or IAM groupsRead-only; respects the underlying data grants
Public linkAnyone with the URLNo sign-in — treat the URL as the secret
Embed keyAny site you allowDomain-restricted; see Web embedding
ExportWhoever you send the file toPDF or PowerPoint for the page, Excel/CSV for the data

Export as a PowerPoint deck

Export → Export to PowerPoint builds a branded deck. You choose which visuals to include (grouped by page), a model to write the prose, and optionally an instruction for tone, audience or emphasis.

The figures are the dashboard's figures. Every slide is filled from the same saved snapshot the card on screen renders — never re-queried while the deck is built. A deck that disagrees with the dashboard it came from is two sources of truth, and the one in the meeting room is the one people act on.

The model writes sentences and is not allowed to calculate. It receives the computed values as text it may quote, so it can write “revenue peaked in March” but not “revenue grew 12%” — the second is arithmetic, and arithmetic is done in code or not at all. Any figure in its prose that did not come from the data is stripped before it reaches a slide. That holds even if you explicitly ask for growth percentages in the instruction box.

A widget with no saved snapshot cannot become a slide and is listed with the reason (refresh the dashboard first) rather than quietly missing from a deck you believe is complete. Visuals PowerPoint cannot draw — sankey, geo map, bar race — become a table of the same data with the substitution printed on the slide, and category or row caps are disclosed on the slide itself.

Check before publishing

A public link removes every access check. Widgets on shared and embedded dashboards are sanitised so their underlying queries aren't exposed, but the data on the page is visible to anyone holding the URL. Publish deliberately.

Row filters and hidden columns on shares

A dashboard grant (Admin → IAM → Access) can carry a row filter (the grantee only sees rows where a column matches allowed values) and hidden columns (columns removed entirely). Both are enforced server-side: a grantee whose grant carries any restriction never reads stored widget data directly — the server applies the filter and drops masked columns before anything leaves it, on snapshots and on live direct queries alike. Semantics follow the rest of IAM: one unrestricted grant (directly or via any group) makes the whole dashboard visible, and a column is hidden only when every applicable grant hides it.

Versioning and promotion

  • Version history — dashboards keep prior versions you can compare and restore.
  • Dev → prod promotion — build in a development folder and promote a reviewed version into production, instead of editing what people are watching.
  • Git export — export dashboard and model definitions as files for review in your own repository.

If a dashboard is slow

  • Switch heavy widgets from direct query to imported snapshots with a refresh schedule. This is usually the whole fix.
  • Turn on Aggregate in SQL. It returns one row per category instead of thousands, so it makes the dashboard faster and the totals correct at the same time.
  • Use incremental refresh if the refresh itself is what's slow rather than the view.
  • Aggregate in a prepared table rather than charting millions of raw rows.
  • Reduce the number of widgets on one page — each is a query.
  • Apply a default date filter so the dashboard doesn't open on all history.