<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Max Mealing</title>
    <description>The latest articles on DEV Community by Max Mealing (@maxbonnard).</description>
    <link>https://dev.to/maxbonnard</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4032337%2F41320bce-53bd-4722-9997-b15a2d6125a0.png</url>
      <title>DEV Community: Max Mealing</title>
      <link>https://dev.to/maxbonnard</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maxbonnard"/>
    <language>en</language>
    <item>
      <title>What Is a Semantic Layer? A Practical Guide for Data Engineers</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:26:01 +0000</pubDate>
      <link>https://dev.to/maxbonnard/what-is-a-semantic-layer-a-practical-guide-for-data-engineers-3i1g</link>
      <guid>https://dev.to/maxbonnard/what-is-a-semantic-layer-a-practical-guide-for-data-engineers-3i1g</guid>
      <description>&lt;p&gt;Your data warehouse has a table called &lt;code&gt;orders&lt;/code&gt;. It has columns like &lt;code&gt;amount&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, and &lt;code&gt;customer_id&lt;/code&gt;. Now three people ask "What was Q1 revenue?"&lt;/p&gt;

&lt;p&gt;The analyst writes &lt;code&gt;SELECT SUM(amount) FROM orders WHERE created_at BETWEEN '2026-01-01' AND '2026-03-31'&lt;/code&gt;. The data engineer adds &lt;code&gt;WHERE status = 'completed'&lt;/code&gt;. Finance excludes refunds and trial conversions. Three queries, three numbers, one question. Nobody is wrong. They just defined "revenue" differently.&lt;/p&gt;

&lt;p&gt;Multiply this by every metric in your organization, every team that queries the warehouse, and every tool that displays a number. That's the problem.&lt;/p&gt;

&lt;p&gt;A semantic layer solves it by defining each metric once, in one place, and serving that definition to every consumer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a semantic layer?
&lt;/h2&gt;

&lt;p&gt;A semantic layer is a metadata layer between your data warehouse and every tool that queries it. It defines business metrics, maps them to SQL, and exposes them through APIs. Instead of every consumer writing its own query, they all reference the same definition.&lt;/p&gt;

&lt;p&gt;When someone asks for "revenue," the semantic layer knows that means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'refunded'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s1"&gt;'trial'&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That definition lives in one place. Dashboards, APIs, AI agents, and ad-hoc queries all use it. Change the definition once and every consumer gets the updated calculation. No Slack thread asking "which number is right." No detective work tracing a wrong number back to a stale query in a notebook somewhere.&lt;/p&gt;

&lt;p&gt;The core components of a semantic layer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metrics (measures).&lt;/strong&gt; The numbers you aggregate: revenue, order count, average deal size. Each metric has a fixed SQL definition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dimensions.&lt;/strong&gt; The columns you filter and group by: date, status, category, region. Dimensions define the axes of analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships (joins).&lt;/strong&gt; How tables connect: orders belong to customers, products belong to categories. Defined once, reused by every query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access rules.&lt;/strong&gt; Who can see what. Row-level security, tenant isolation, role-based access. Enforced on every query, not bolted on per tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching rules.&lt;/strong&gt; Which rollups to pre-compute, how often to refresh, when to invalidate. Performance is part of the definition.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A brief history
&lt;/h2&gt;

&lt;p&gt;The idea of abstracting business logic from raw data isn't new.&lt;/p&gt;

&lt;p&gt;In the 1990s, &lt;strong&gt;Business Objects&lt;/strong&gt; introduced "universes," metadata layers that mapped business terms to database columns. Analysts queried "Revenue by Region" without knowing the underlying table structure. It worked, but only within Business Objects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSAS (SQL Server Analysis Services)&lt;/strong&gt; cubes brought multidimensional modeling to the Microsoft stack. You defined measures and dimensions in a cube, and every Excel pivot table and SSRS report consumed the same definitions. Powerful, but tightly coupled to the Microsoft ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Looker's LookML&lt;/strong&gt; (2012) moved semantic modeling into code. Metric definitions lived in version-controlled files, not a GUI. This was a significant shift: data teams could review metric changes in pull requests, not point-and-click editors. But LookML was (and is) proprietary to Looker, which is now proprietary to Google Cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dbt's MetricFlow&lt;/strong&gt; (2023) brought metric definitions into the dbt ecosystem. Define metrics alongside your transformations. Good idea, but MetricFlow defines metrics without serving them: no caching, no multi-tenancy, no API layer.&lt;/p&gt;

&lt;p&gt;The current generation of semantic layers (Cube, AtScale) are standalone infrastructure. They connect to any warehouse, serve any consumer, and run independently of your BI tool. The semantic layer is no longer a feature inside a product. It's a layer in the stack.&lt;/p&gt;

&lt;p&gt;What's driving this shift: the number of consumers has exploded. In 2015, a semantic layer served dashboards. In 2026, it also serves &lt;a href="https://bonnard.dev/glossary/mcp" rel="noopener noreferrer"&gt;AI agents via MCP&lt;/a&gt;. One definition needs to serve all of them, and the newest consumer is an agent that queries metrics and then wants to chart the result in the conversation. A BI-embedded semantic layer can't reach that consumer at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does it matter?
&lt;/h2&gt;

&lt;p&gt;Without a semantic layer, business logic lives in three places: your analysts' heads, scattered SQL files, and buried dashboard definitions. Each copy drifts over time. Someone updates the revenue definition in Looker but forgets the dbt model. A new analyst writes a fresh query from scratch and gets a number that doesn't match either.&lt;/p&gt;

&lt;p&gt;The cost isn't abstract:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inconsistent metrics.&lt;/strong&gt; Marketing reports 12% growth. Finance reports 8%. The board meeting stalls while someone debugs which number is right. This happens every quarter at companies without a shared metric layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicated logic.&lt;/strong&gt; Every new dashboard, API endpoint, or report re-implements the same calculations. You end up with revenue defined in 14 places, each slightly different, each maintained by a different person. One team discovers a bug in their version and fixes it. The other 13 don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slow onboarding.&lt;/strong&gt; New data engineers spend weeks learning which queries are canonical and which are stale. "Ask Sarah, she knows which revenue query is the right one." Tribal knowledge doesn't scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data team becomes a service desk.&lt;/strong&gt; Instead of building, they spend their time answering ad-hoc questions: "Can you pull revenue by region for Q4?" "Which dashboard has the right churn number?" "Why does this report show different numbers than that one?" A semantic layer makes the data self-serve. The team defines the metrics. Everyone else queries them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI agents make it worse.&lt;/strong&gt; An AI agent with warehouse access generates plausible SQL from column names. It doesn't know your business rules. Ask two different agents the same question and you'll get two different numbers. Neither matches finance. &lt;a href="https://bonnard.dev/glossary/text-to-sql" rel="noopener noreferrer"&gt;Text-to-SQL&lt;/a&gt; gives you speed without trust. A semantic layer gives you both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of semantic layers
&lt;/h2&gt;

&lt;p&gt;Not all semantic layers work the same way. The architecture matters because it determines what consumers you can serve, how fast queries run, and how much you control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embedded semantic layers&lt;/strong&gt; live inside a BI tool. Looker's LookML, Tableau's semantic model, Power BI's DAX measures. They define metrics well, but only for that tool's consumers. Your Looker semantic layer doesn't help your React app or your AI agent. If you're in one BI tool and staying there, this can be enough. Most teams outgrow it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Virtual semantic layers&lt;/strong&gt; sit between the warehouse and consumers without moving data. AtScale is the primary example. Queries pass through the semantic layer, which translates them to the right SQL dialect and sends them to the warehouse. No data duplication. The tradeoff: every query hits the warehouse, so latency depends on warehouse performance. Virtual layers add caching to mitigate this, but it's an optimization, not the default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Materialized semantic layers&lt;/strong&gt; pre-compute rollups and store them separately. Cube's pre-aggregation engine is the best-known example. You define which combinations of measures and dimensions to pre-compute, and the semantic layer builds and maintains those materialized tables. Hot queries hit the cache (single-digit milliseconds). Cold queries fall through to the warehouse. This is how you get sub-second performance at scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hybrid semantic layers&lt;/strong&gt; combine virtual and materialized approaches. Most modern semantic layers work this way. Define pre-aggregations for your most-queried metrics. Let everything else pass through to the warehouse. You configure the tradeoff per metric based on query frequency and latency requirements.&lt;/p&gt;

&lt;p&gt;For most teams, hybrid is the right choice. You get the flexibility of virtual queries with the performance of materialized caching where it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works in practice
&lt;/h2&gt;

&lt;p&gt;Modern semantic layers are defined in code (typically YAML), version-controlled in Git, and deployed as an API. Here's what that looks like end to end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define your metrics
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cubes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
    &lt;span class="na"&gt;sql_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public.orders&lt;/span&gt;
    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_revenue&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CASE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;WHEN&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'refunded'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AND&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'trial'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;THEN&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ELSE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;END"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_count&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;average_order_value&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amount&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;avg&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;category&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;category&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;time&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customer_id&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Measures&lt;/strong&gt; are the numbers you aggregate: revenue, count, average. &lt;strong&gt;Dimensions&lt;/strong&gt; are the columns you filter and group by: status, date, category. &lt;code&gt;total_revenue&lt;/code&gt; is now a governed definition, not a column someone interprets.&lt;/p&gt;

&lt;p&gt;You define joins between cubes to model relationships:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cubes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;customers&lt;/span&gt;
    &lt;span class="na"&gt;sql_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public.customers&lt;/span&gt;
    &lt;span class="na"&gt;joins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
        &lt;span class="na"&gt;relationship&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;one_to_many&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{customers.id}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{orders.customer_id}"&lt;/span&gt;
    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;signed_up_at&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now any consumer can query "revenue by customer plan" without knowing how the tables join. The semantic layer handles the SQL generation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add caching with pre-aggregations
&lt;/h3&gt;

&lt;p&gt;Queries against raw warehouse tables are expensive, especially when AI agents are querying them frequently. A &lt;a href="https://bonnard.dev/glossary/pre-aggregation" rel="noopener noreferrer"&gt;pre-aggregation&lt;/a&gt; caches rollups so consumers get sub-second responses instead of waiting for full table scans.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;pre_aggregations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daily_revenue&lt;/span&gt;
        &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;total_revenue&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;order_count&lt;/span&gt;
        &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
        &lt;span class="na"&gt;time_dimension&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;granularity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;day&lt;/span&gt;
        &lt;span class="na"&gt;refresh_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;every&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1 hour&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The semantic layer rebuilds this rollup every hour. Hot queries hit the cache. Cold queries fall through to the warehouse. You configure the tradeoff per metric: high-traffic metrics get aggressive caching, low-traffic metrics query the warehouse directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define access control
&lt;/h3&gt;

&lt;p&gt;For B2B products serving multiple customers, every query needs to be scoped to the right tenant. A security context enforces this structurally, not through prompt engineering or middleware hacks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;security_context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tenant_filter&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{SECURITY_CONTEXT.tenant_id}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;customer_id"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every query for a given tenant automatically includes this filter. It's not optional. The consumer can't skip it or override it. This is how &lt;a href="https://bonnard.dev/glossary/rbac" rel="noopener noreferrer"&gt;RBAC&lt;/a&gt; works in a semantic layer: defined in the schema, enforced on every query. It doesn't matter if the consumer is a dashboard, an API call, or an AI agent. The access rules are the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expose to consumers
&lt;/h3&gt;

&lt;p&gt;One set of definitions, multiple consumers. A BI dashboard queries via REST API. A backend service calls an SDK. An AI agent discovers and queries metrics over &lt;a href="https://bonnard.dev/glossary/mcp" rel="noopener noreferrer"&gt;MCP&lt;/a&gt;. They all hit the same definitions and get the same numbers. That's the point.&lt;/p&gt;

&lt;p&gt;The agent is the consumer that changed the most. It queries a metric, then wants to show the result, not as a wall of numbers but as a chart in the conversation. That's where &lt;a href="https://www.npmjs.com/package/@bonnard/mcp-charts" rel="noopener noreferrer"&gt;&lt;code&gt;@bonnard/mcp-charts&lt;/code&gt;&lt;/a&gt; comes in: a &lt;code&gt;visualize&lt;/code&gt; tool you add to your MCP server that renders interactive charts from your query results, right in Claude or ChatGPT.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// rows come back from your warehouse, the chart renders in the client&lt;/span&gt;
&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runSql&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent calls &lt;code&gt;visualize&lt;/code&gt; with a query, your &lt;code&gt;runSql&lt;/code&gt; returns the rows, Bonnard infers the chart from the typed result, and an interactive widget renders in the host. You write no frontend code, and Bonnard never touches your database. More in the &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP Charts guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-world use cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Customer-facing analytics in a B2B SaaS product
&lt;/h3&gt;

&lt;p&gt;You're building a SaaS product and your customers want to see their data. Usage metrics, billing summaries, performance dashboards. Without a semantic layer, you build custom API endpoints for each view, maintain the SQL yourself, and hope the numbers match what your internal dashboards show.&lt;/p&gt;

&lt;p&gt;With a semantic layer: define the metrics once with row-level security, and let customers query their data through your product's UI or through their own AI agents (via MCP). Multi-tenancy is structural. The same metric definition serves every customer with automatic data isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replacing internal dashboards nobody opens
&lt;/h3&gt;

&lt;p&gt;Your team built 47 Metabase dashboards. Three of them get used. The rest are stale, broken, or duplicated. The data team spends time maintaining dashboard infrastructure instead of defining the metrics that matter.&lt;/p&gt;

&lt;p&gt;A semantic layer shifts the model: the data team defines and governs metrics. Consumers choose their own surface. Some use dashboards. Some query the API. Some ask an AI agent. The data team's job is the definitions, not the delivery mechanism.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting AI agents to governed data
&lt;/h3&gt;

&lt;p&gt;Your engineering team wants to ship an AI-powered analytics feature. The first prototype uses text-to-SQL. It works in demos and breaks in production: inconsistent numbers, no tenant isolation, no audit trail. The fix isn't better prompts. It's a semantic layer. The agent queries metric definitions instead of raw tables. Every answer traces back to a versioned, governed definition. Full walkthrough: &lt;a href="https://bonnard.dev/blog/connect-ai-agent-data-warehouse" rel="noopener noreferrer"&gt;How to Connect an AI Agent to Your Data Warehouse&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semantic layer vs. dbt metrics
&lt;/h2&gt;

&lt;p&gt;This is the most common question in the space, and the answer is: they're complementary, not competing. But the differences matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dbt&lt;/strong&gt; defines transformations: how raw data becomes clean tables. It runs SQL models that create &lt;code&gt;dim_customers&lt;/code&gt;, &lt;code&gt;fct_orders&lt;/code&gt;, and other modeled tables in your warehouse. This is the "T" in ELT. dbt is excellent at this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;dbt's MetricFlow&lt;/strong&gt; adds metric definitions on top of those tables. You can define &lt;code&gt;revenue&lt;/code&gt; as a metric in your dbt project. This is genuinely useful for documentation and consistency within dbt's ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where dbt stops:&lt;/strong&gt; MetricFlow defines metrics but doesn't serve them to production consumers. There's no built-in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Caching or pre-aggregation.&lt;/strong&gt; Every query hits the warehouse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenancy.&lt;/strong&gt; No per-tenant isolation for B2B use cases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API serving.&lt;/strong&gt; No REST API or SDK for applications to query.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI agent integration.&lt;/strong&gt; No MCP server or tool-use interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Row-level security.&lt;/strong&gt; Access control is warehouse-level, not metric-level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A standalone semantic layer fills these gaps. It connects to the same warehouse where dbt outputs its modeled tables, defines metrics on top, and serves them with caching, multi-tenancy, and multiple APIs.&lt;/p&gt;

&lt;p&gt;The practical workflow: dbt transforms your raw data into clean tables on a schedule. The semantic layer defines business metrics on those tables and serves them to dashboards, applications, and AI agents in real time. Many semantic layers can import dbt models directly. You don't choose between them. You use both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semantic layer tools compared
&lt;/h2&gt;

&lt;p&gt;Several tools occupy this space, each with different design priorities.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Strengths&lt;/th&gt;
&lt;th&gt;Limitations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://cube.dev" rel="noopener noreferrer"&gt;Cube&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open-source semantic layer engine&lt;/td&gt;
&lt;td&gt;Battle-tested query engine, pre-aggregation, REST API, large community&lt;/td&gt;
&lt;td&gt;No built-in MCP, dashboards, or multi-tenant keys. You build the product layer yourself.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;dbt Semantic Layer / MetricFlow&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Metric definitions in dbt&lt;/td&gt;
&lt;td&gt;Tight dbt integration, SQL-native, good for documentation&lt;/td&gt;
&lt;td&gt;Defines metrics but doesn't serve them. No caching, no multi-tenancy, no agent support.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Looker (LookML)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BI-embedded semantic layer&lt;/td&gt;
&lt;td&gt;Mature modeling language, strong governance, Google Cloud integration&lt;/td&gt;
&lt;td&gt;Expensive, enterprise-only, proprietary. Definitions locked to Looker's ecosystem. No MCP.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AtScale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Virtual semantic layer&lt;/td&gt;
&lt;td&gt;Enterprise BI compatibility, no data movement, MDX/DAX support&lt;/td&gt;
&lt;td&gt;Enterprise pricing. Not agent-native. Complex deployment.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Power BI (DAX)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BI-embedded semantic layer&lt;/td&gt;
&lt;td&gt;Deep Microsoft integration, familiar to analysts&lt;/td&gt;
&lt;td&gt;Locked to Power BI ecosystem. No API serving. No multi-tenancy for embedded use cases.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tableau (Semantic Model)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BI-embedded semantic layer&lt;/td&gt;
&lt;td&gt;Visual modeling, strong visualization&lt;/td&gt;
&lt;td&gt;Definitions don't leave Tableau. No API access. Salesforce pricing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://bonnard.dev" rel="noopener noreferrer"&gt;Bonnard&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Interactive charts via the MCP &lt;code&gt;visualize&lt;/code&gt; tool&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; adds a &lt;code&gt;visualize&lt;/code&gt; tool to your MCP server, rendering charts in Claude/ChatGPT from your query results. No frontend code.&lt;/td&gt;
&lt;td&gt;Charts the result, not a full semantic layer engine. Pair it with one.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The right choice depends on your primary consumer. If internal dashboards are the only output and you're already in Looker or Power BI, the embedded semantic layer may be sufficient. If you're serving multiple consumers (customer-facing products, AI agents, APIs, internal dashboards), you need a standalone semantic layer that isn't locked to one tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Warehouse-specific considerations
&lt;/h2&gt;

&lt;p&gt;Your semantic layer needs to work with your warehouse. The integration isn't just "can it connect." It's how well the semantic layer exploits each warehouse's strengths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bonnard.dev/integrations/snowflake" rel="noopener noreferrer"&gt;Snowflake&lt;/a&gt;.&lt;/strong&gt; Pre-aggregations work well with Snowflake's auto-suspend compute. The semantic layer can target specific virtual warehouses for different workloads: fast cache rebuilds on a small warehouse, heavy ad-hoc queries on a larger one. Snowflake's separation of storage and compute means pre-aggregation tables don't compete with production workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bonnard.dev/integrations/bigquery" rel="noopener noreferrer"&gt;BigQuery&lt;/a&gt;.&lt;/strong&gt; BigQuery's slot-based pricing means pre-aggregations save real money. Every query scans data and costs money. Caching a daily revenue rollup avoids scanning terabytes of raw data on every request. The semantic layer's pre-aggregation layer can cut BigQuery costs by 10-100x for frequently queried metrics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bonnard.dev/integrations/databricks" rel="noopener noreferrer"&gt;Databricks&lt;/a&gt;.&lt;/strong&gt; Unity Catalog provides table-level governance. The semantic layer adds metric-level governance on top: defining what "revenue" means, not just who can access the &lt;code&gt;orders&lt;/code&gt; table. Databricks' Photon engine handles the heavy queries. The semantic layer caches the results.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bonnard.dev/integrations/postgres" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt; (including Supabase, Neon, and RDS).&lt;/strong&gt; Works well for smaller datasets and development. Most teams start with Postgres for prototyping their semantic layer and migrate to a cloud warehouse as data volume grows. Pre-aggregation is especially valuable here because Postgres doesn't have the raw compute power of cloud warehouses for large analytical queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://bonnard.dev/integrations/duckdb" rel="noopener noreferrer"&gt;DuckDB&lt;/a&gt; (including MotherDuck).&lt;/strong&gt; In-process analytical database. Useful for local development and testing without a remote warehouse connection. Define your metrics, test locally against DuckDB, deploy to production against Snowflake or BigQuery. Same schema, different warehouses.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to evaluate a semantic layer
&lt;/h2&gt;

&lt;p&gt;If you're choosing a semantic layer for your stack, here's what to look at:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who are your consumers?&lt;/strong&gt; This is the most important question. If it's only internal dashboards, an embedded semantic layer in your BI tool might be enough. If it's dashboards plus a customer-facing product plus AI agents, you need a standalone semantic layer with API serving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does it support your warehouse?&lt;/strong&gt; Check for native support, not just "we can connect via JDBC." Native support means the semantic layer generates optimized SQL for your specific warehouse dialect and exploits warehouse-specific features like Snowflake's clustering or BigQuery's partitioning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does it handle caching?&lt;/strong&gt; Pre-aggregation support varies widely. Some tools don't cache at all (every query hits the warehouse). Some cache at the query level. The best cache at the metric level with configurable refresh schedules and automatic invalidation. If AI agents are querying your data, caching is not optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenancy.&lt;/strong&gt; If you're building a B2B product, every query needs tenant isolation. Look for structural multi-tenancy (security context in the schema), not middleware-level filtering. One missed filter is a data leak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema-as-code.&lt;/strong&gt; Metric definitions should live in version control. Changes should go through pull requests. Rollbacks should be &lt;code&gt;git revert&lt;/code&gt;, not clicking through a UI. If you can't diff your metric definitions, you can't govern them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI agent support.&lt;/strong&gt; If AI agents are current or future consumers, look for &lt;a href="https://bonnard.dev/glossary/mcp" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; support or a tool-use interface. An API is the minimum. MCP is the emerging standard for agent-to-tool communication and gives agents native discovery of available metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  When do you need a semantic layer?
&lt;/h2&gt;

&lt;p&gt;Not every team needs one. Here's when it starts to matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need one when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple consumers query the same metrics (dashboards + API + agents)&lt;/li&gt;
&lt;li&gt;Different teams report different numbers for the same question&lt;/li&gt;
&lt;li&gt;You're shipping analytics to customers (B2B &lt;a href="https://bonnard.dev/glossary/embedded-analytics" rel="noopener noreferrer"&gt;embedded analytics&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;AI agents need access to your data with governance&lt;/li&gt;
&lt;li&gt;New analysts spend days learning which queries are canonical&lt;/li&gt;
&lt;li&gt;Your data team is stuck answering ad-hoc metric questions instead of building&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You probably don't need one when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One analyst queries the warehouse with SQL and shares results in spreadsheets&lt;/li&gt;
&lt;li&gt;You have a single dashboard tool and no API consumers&lt;/li&gt;
&lt;li&gt;Your data model is small enough that everyone knows the canonical queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The threshold is usually "more than one consumer for the same metrics." Once dashboards, APIs, and applications all need revenue numbers, a shared definition pays for itself immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;If you're evaluating options, start with what your primary consumer needs. Internal dashboards only? Your BI tool's built-in semantic layer may be enough. dbt-native workflow with documentation as the goal? Start with MetricFlow. Multiple consumers including AI agents? A standalone semantic layer like &lt;a href="https://cube.dev" rel="noopener noreferrer"&gt;Cube&lt;/a&gt; or &lt;a href="https://www.atscale.com" rel="noopener noreferrer"&gt;AtScale&lt;/a&gt;, with metrics defined once and served over an API.&lt;/p&gt;

&lt;p&gt;When AI agents are a consumer, the last mile is the chart. An agent queries a metric, then needs to show the result in the conversation. Add that with one tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @bonnard/mcp-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runSql&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;addCharts&lt;/code&gt; registers a &lt;code&gt;visualize&lt;/code&gt; tool on your MCP server. The agent calls it with a query, your &lt;code&gt;runSql&lt;/code&gt; returns the rows, and Bonnard renders an interactive chart in Claude or ChatGPT. Bonnard never touches your database. Repo: &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;github.com/bonnard-data/mcp-charts&lt;/a&gt;. Full walkthrough: &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP Charts&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a semantic layer in simple terms?
&lt;/h3&gt;

&lt;p&gt;A semantic layer is a single place where you define what business terms like "revenue" or "active user" mean in terms of database columns and calculations. Every tool that needs that number references the same definition instead of writing its own query. One definition, every consumer, same answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between a semantic layer and a data warehouse?
&lt;/h3&gt;

&lt;p&gt;A data warehouse stores your data. A semantic layer defines what the data means. The warehouse has a column called &lt;code&gt;amount&lt;/code&gt;. The semantic layer defines that &lt;code&gt;total_revenue = SUM(amount) WHERE status != 'refunded'&lt;/code&gt;. They work together: the warehouse holds the data, the semantic layer holds the business logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is dbt a semantic layer?
&lt;/h3&gt;

&lt;p&gt;dbt is primarily a transformation tool that models raw data into clean tables. dbt's MetricFlow adds metric definition capabilities, which is one part of a semantic layer. But a full semantic layer also includes caching, multi-tenancy, access control, and API serving, which MetricFlow doesn't provide on its own. Most teams use dbt for transformations and a separate semantic layer for metric serving.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between a semantic layer and an OBT (One Big Table)?
&lt;/h3&gt;

&lt;p&gt;An OBT flattens everything into a single denormalized table. A semantic layer keeps your data normalized and defines metrics as calculations across tables. OBTs are simpler at first but break down at scale: they're expensive to rebuild, hard to govern, and create massive tables with redundant data. A semantic layer gives you the same query simplicity without the storage and maintenance cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between a semantic layer and a metrics layer?
&lt;/h3&gt;

&lt;p&gt;The terms are often used interchangeably, but a metrics layer is a subset of a semantic layer. A metrics layer defines measures and dimensions. A semantic layer adds relationships (joins), access control, caching, and API serving on top. dbt's MetricFlow is a metrics layer. Cube and AtScale are full semantic layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need a semantic layer for AI agents?
&lt;/h3&gt;

&lt;p&gt;If AI agents query your data, a semantic layer is the difference between "plausible numbers" and "correct numbers." Without one, agents interpret column names and generate ad-hoc SQL. With one, agents query governed metric definitions and get the same answer every time. See &lt;a href="https://bonnard.dev/blog/what-is-agentic-semantic-layer" rel="noopener noreferrer"&gt;What Is an Agentic Semantic Layer?&lt;/a&gt; for the full breakdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does a semantic layer handle performance?
&lt;/h3&gt;

&lt;p&gt;Through &lt;a href="https://bonnard.dev/glossary/pre-aggregation" rel="noopener noreferrer"&gt;pre-aggregation&lt;/a&gt;. The semantic layer pre-computes common rollups (daily revenue by status, weekly order count by category) and caches the results. Queries hit the cache instead of scanning raw tables. Hot queries resolve in single-digit milliseconds. The cache rebuilds on a configurable schedule with automatic invalidation when source data changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use a semantic layer with Snowflake, BigQuery, and other warehouses?
&lt;/h3&gt;

&lt;p&gt;Yes. Modern semantic layers are warehouse-agnostic. You configure the connection to your warehouse (&lt;a href="https://bonnard.dev/integrations/snowflake" rel="noopener noreferrer"&gt;Snowflake&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/bigquery" rel="noopener noreferrer"&gt;BigQuery&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/databricks" rel="noopener noreferrer"&gt;Databricks&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/postgres" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt; (including Supabase, Neon, and RDS), &lt;a href="https://bonnard.dev/integrations/redshift" rel="noopener noreferrer"&gt;Redshift&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/duckdb" rel="noopener noreferrer"&gt;DuckDB&lt;/a&gt; (including MotherDuck)) and the semantic layer generates the appropriate SQL dialect. Swap warehouses without changing your metric definitions.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a universal semantic layer?
&lt;/h3&gt;

&lt;p&gt;A universal semantic layer serves every consumer from one set of definitions, regardless of what tool they use. Instead of defining metrics separately in Looker, Power BI, and your application code, you define them once and serve them everywhere. "Universal" means tool-agnostic: the semantic layer isn't embedded in any single BI product.&lt;/p&gt;

</description>
      <category>semanticlayer</category>
      <category>dataengineering</category>
      <category>database</category>
      <category>data</category>
    </item>
    <item>
      <title>Turn Your dbt Project Into a Semantic Layer for Agents</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:25:58 +0000</pubDate>
      <link>https://dev.to/maxbonnard/turn-your-dbt-project-into-a-semantic-layer-for-agents-2alc</link>
      <guid>https://dev.to/maxbonnard/turn-your-dbt-project-into-a-semantic-layer-for-agents-2alc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Draft placeholder. Full article coming soon.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You already have dbt models, tests, and a manifest. This post will show how to turn that into a governed semantic layer agents can query, without rebuilding your stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this will cover
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Reading your dbt manifest to bootstrap metrics and relationships&lt;/li&gt;
&lt;li&gt;Defining metrics once so every consumer gets the same number&lt;/li&gt;
&lt;li&gt;Tagging the tenant key that scopes each customer's rows&lt;/li&gt;
&lt;li&gt;Exposing governed metrics to agents over MCP&lt;/li&gt;
&lt;li&gt;Charting the agent's query results in Claude or ChatGPT with &lt;a href="https://www.npmjs.com/package/@bonnard/mcp-charts" rel="noopener noreferrer"&gt;&lt;code&gt;@bonnard/mcp-charts&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need to migrate off dbt?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. A semantic layer reads your existing dbt models. You keep dbt as your transformation layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if I only have models, not a semantic layer?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the common case. You bootstrap from the manifest and define metrics on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do agents chart the results?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add a &lt;code&gt;visualize&lt;/code&gt; tool to your MCP server with &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt;. The agent queries a metric, your &lt;code&gt;runSql&lt;/code&gt; returns the rows, and an interactive chart renders in Claude or ChatGPT. Bonnard never touches your database. See the &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP Charts guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>dbt</category>
      <category>semanticlayer</category>
      <category>data</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Self-Service BI Is a Lie (Unless You Govern the Metrics)</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:25:55 +0000</pubDate>
      <link>https://dev.to/maxbonnard/self-service-bi-is-a-lie-unless-you-govern-the-metrics-2o6</link>
      <guid>https://dev.to/maxbonnard/self-service-bi-is-a-lie-unless-you-govern-the-metrics-2o6</guid>
      <description>&lt;p&gt;Self-service BI was supposed to free the data team. Give everyone a BI tool, teach them to drag and drop, and they'll answer their own questions. The data team can stop building dashboards and focus on infrastructure.&lt;/p&gt;

&lt;p&gt;That's not what happened.&lt;/p&gt;

&lt;p&gt;What happened: everyone builds their own dashboards with their own metric definitions. Marketing's "active users" counts monthly logins. Product's "active users" counts weekly feature usage. Finance's "active users" counts paying customers. Three dashboards, three numbers, one term. The data team now spends their time reconciling conflicting metrics instead of building.&lt;/p&gt;

&lt;p&gt;Self-service BI without governed metrics is just self-service chaos.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is self-service BI?
&lt;/h2&gt;

&lt;p&gt;Self-service BI means non-technical users can query data, build visualizations, and generate reports without help from the data team. The tools (Metabase, Looker, Power BI, Tableau, Superset) provide drag-and-drop interfaces, visual query builders, and template libraries.&lt;/p&gt;

&lt;p&gt;The promise: democratize data access. Anyone can answer their own questions.&lt;/p&gt;

&lt;p&gt;The reality: it works for simple questions ("how many orders this week?") and breaks for anything requiring business logic ("what's our net revenue retention?"). Users either define metrics incorrectly or ask the data team anyway. The data team becomes a help desk for the self-service tool instead of a help desk for SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where self-service BI goes wrong
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Metric sprawl
&lt;/h3&gt;

&lt;p&gt;Without a central definition, every self-service user creates their own version of key metrics. A company with 50 Metabase users might have 30 different "revenue" calculations saved across collections. Which one is right? The one that matches the board report. Which one matches the board report? Nobody knows without checking each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "someone who knows" bottleneck
&lt;/h3&gt;

&lt;p&gt;True self-service requires understanding the data model. Which table has revenue? What does &lt;code&gt;status = 3&lt;/code&gt; mean? Is the &lt;code&gt;amount&lt;/code&gt; column in cents or dollars? Pre-tax or post-tax? Including shipping or excluding?&lt;/p&gt;

&lt;p&gt;Without this context, self-service users either guess (and get wrong answers) or ask the data team (and it's not self-service). The BI tool gives access to data. It doesn't give understanding of what the data means.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dashboard proliferation
&lt;/h3&gt;

&lt;p&gt;Every self-service user creates dashboards for their use case. The dashboard count grows from 10 to 500. Most are stale. Many are duplicates. Finding the right dashboard becomes its own research project. The data team periodically "cleans up," deleting abandoned dashboards and breaking someone's workflow each time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open source BI tools and their limitations
&lt;/h3&gt;

&lt;p&gt;Open-source BI tools make self-service accessible without enterprise licensing. The main options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://bonnard.dev/vs-metabase" rel="noopener noreferrer"&gt;Metabase&lt;/a&gt;&lt;/strong&gt; (AGPL): Most popular. Visual query builder, drag-and-drop dashboards. No semantic layer. Multi-tenancy requires Enterprise license.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://bonnard.dev/vs-superset" rel="noopener noreferrer"&gt;Superset&lt;/a&gt;&lt;/strong&gt; (Apache 2.0): SQL-native, powerful for technical teams. No semantic layer. Limited multi-tenancy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redash&lt;/strong&gt; (BSD): SQL editor focused. Simple, lightweight. No governance features. Maintenance mode since Databricks acquired it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightdash&lt;/strong&gt; (MIT): dbt-native. Good for teams already using dbt. Growing community. No MCP or embedded SDK.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence&lt;/strong&gt; (Apache 2.0): Code-first reporting. Markdown + SQL. Good for scheduled reports. Not designed for embedded or multi-tenant use cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They share the same fundamental limitation: no &lt;a href="https://bonnard.dev/glossary/semantic-layer" rel="noopener noreferrer"&gt;semantic layer&lt;/a&gt;. Users query raw tables and define metrics ad-hoc. The open-source tools democratize access. They don't democratize understanding.&lt;/p&gt;

&lt;h2&gt;
  
  
  What real self-service looks like
&lt;/h2&gt;

&lt;p&gt;Real self-service means anyone can query data and get the right answer. Not "anyone can query data and get an answer." The difference is governance.&lt;/p&gt;

&lt;h3&gt;
  
  
  The data team defines metrics
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cubes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue&lt;/span&gt;
    &lt;span class="na"&gt;sql_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analytics.revenue&lt;/span&gt;
    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_revenue&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CASE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;WHEN&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'refunded'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AND&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'trial'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;THEN&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ELSE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;END"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Finance-approved&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;revenue&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;definition&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(excludes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;refunds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;trials)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;net_revenue_retention&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_period_mrr&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prior_period_mrr"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;avg&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NRR&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;existing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;customer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cohorts"&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan_name&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;period&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;period_date&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These definitions live in Git. Finance approves them in a pull request. When the definition changes, every consumer gets the update. There's one "revenue." Not 30.&lt;/p&gt;

&lt;h3&gt;
  
  
  Everyone queries the same definitions
&lt;/h3&gt;

&lt;p&gt;The product manager asks an AI agent: "What's revenue by plan this quarter?" The agent calls &lt;code&gt;explore_schema&lt;/code&gt;, finds &lt;code&gt;revenue.total_revenue&lt;/code&gt;, and queries it. Same definition finance approved.&lt;/p&gt;

&lt;p&gt;The customer success team views an &lt;a href="https://bonnard.dev/glossary/embedded-analytics" rel="noopener noreferrer"&gt;embedded analytics&lt;/a&gt; dashboard in the product. Same definition.&lt;/p&gt;

&lt;p&gt;The CEO checks a markdown dashboard. Same definition.&lt;/p&gt;

&lt;p&gt;Nobody re-implements the metric. Nobody guesses which table to query. The data team defined it once. Every surface serves it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The data team builds, not debugs
&lt;/h3&gt;

&lt;p&gt;With governed metrics, the data team's job shifts from "answer ad-hoc questions" and "reconcile conflicting dashboards" to "define and maintain metric definitions." They add new metrics, refine existing ones, and optimize &lt;a href="https://bonnard.dev/glossary/pre-aggregation" rel="noopener noreferrer"&gt;pre-aggregation&lt;/a&gt; strategies. The mechanical work (querying, formatting, presenting) is handled by the semantic layer and its consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-service BI tools compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Self-service model&lt;/th&gt;
&lt;th&gt;Governed metrics&lt;/th&gt;
&lt;th&gt;AI agent support&lt;/th&gt;
&lt;th&gt;Open source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Metabase&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Visual query builder&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (AGPL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Superset&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SQL + visual builder&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (Apache 2.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redash&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SQL editor&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Power BI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drag-and-drop&lt;/td&gt;
&lt;td&gt;DAX (PBI only)&lt;/td&gt;
&lt;td&gt;Copilot&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tableau&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Drag-and-drop&lt;/td&gt;
&lt;td&gt;Tableau model&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Looker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LookML + Explore&lt;/td&gt;
&lt;td&gt;LookML&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ThoughtSpot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Natural language search&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;Proprietary&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Charts in AI agent (Bonnard)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Charts inside Claude or ChatGPT via MCP&lt;/td&gt;
&lt;td&gt;N/A (your &lt;code&gt;runSql&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Native (&lt;code&gt;visualize&lt;/code&gt; tool)&lt;/td&gt;
&lt;td&gt;Yes (npm)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tools in the top half give users direct data access with no metric governance. The tools in the bottom half add governance but lock it to their ecosystem. The bottom row is a different kind of option: &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; does not replace a BI tool. It adds a &lt;code&gt;visualize&lt;/code&gt; tool to your MCP server so customers get interactive charts inside Claude or ChatGPT, rendered from your own query results.&lt;/p&gt;

&lt;h2&gt;
  
  
  When traditional self-service BI is enough
&lt;/h2&gt;

&lt;p&gt;Self-service BI works when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small team (&amp;lt; 10 analysts) agrees on metric definitions informally&lt;/li&gt;
&lt;li&gt;One BI tool serves all consumers&lt;/li&gt;
&lt;li&gt;Internal use only (no customers seeing the numbers)&lt;/li&gt;
&lt;li&gt;The data model is simple enough that users understand it&lt;/li&gt;
&lt;li&gt;You have someone who maintains a "source of truth" dashboard that others copy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It breaks when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple teams define the same metric differently&lt;/li&gt;
&lt;li&gt;You need metrics in dashboards AND APIs AND AI agents AND embedded analytics&lt;/li&gt;
&lt;li&gt;Customers see the numbers (trust and consistency matter)&lt;/li&gt;
&lt;li&gt;The data team spends more time reconciling than building&lt;/li&gt;
&lt;li&gt;New hires take weeks to find the right dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Self-service for customers, inside the agent
&lt;/h2&gt;

&lt;p&gt;Governed metrics fix the consistency problem for internal self-service. There's a separate question for customer-facing self-service: where does the customer ask their question? Increasingly the answer is an AI agent. They want to ask about their data in Claude or ChatGPT and get a chart back.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; covers that surface. It is not a BI tool or a dashboard product. It is a &lt;code&gt;visualize&lt;/code&gt; tool you add to your MCP server that renders interactive charts from your query results, inside the agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @bonnard/mcp-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runSql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;addCharts&lt;/code&gt; registers a &lt;code&gt;visualize&lt;/code&gt; tool (and &lt;code&gt;visualize_read_me&lt;/code&gt;). The agent calls &lt;code&gt;visualize&lt;/code&gt;, your &lt;code&gt;runSql&lt;/code&gt; returns rows, Bonnard infers the chart from the typed result, and it renders an interactive &lt;code&gt;ui://&lt;/code&gt; widget in Claude or ChatGPT. Chart types: line, bar, area, pie, scatter, funnel, waterfall, and table. Native adapters cover Postgres, BigQuery, Snowflake, Databricks, and DuckDB, or pass your own &lt;code&gt;runSql&lt;/code&gt;. Bonnard never touches your database.&lt;/p&gt;

&lt;p&gt;For background: &lt;a href="https://bonnard.dev/blog/what-is-a-semantic-layer" rel="noopener noreferrer"&gt;What Is a Semantic Layer?&lt;/a&gt;. For the full walkthrough: &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP charts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;github.com/bonnard-data/mcp-charts&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is self-service BI?
&lt;/h3&gt;

&lt;p&gt;Self-service BI is a BI model where non-technical users can query data, build visualizations, and generate reports without help from the data team. Tools like Metabase, Power BI, and Tableau provide drag-and-drop interfaces for this. The limitation: self-service without governed metrics leads to inconsistent numbers across the organization.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the best open-source BI tool?
&lt;/h3&gt;

&lt;p&gt;For internal dashboards: &lt;a href="https://bonnard.dev/vs-metabase" rel="noopener noreferrer"&gt;Metabase&lt;/a&gt; (most popular), Superset (more technical), or Redash (SQL-focused). For governed metrics served to multiple surfaces including AI agents: a semantic layer approach. Open-source BI tools give you self-service querying. A semantic layer gives you self-service with correct, governed answers.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between self-service BI and a semantic layer?
&lt;/h3&gt;

&lt;p&gt;Self-service BI gives users tools to query data. A semantic layer defines what the data means. They're complementary: a self-service BI tool with a semantic layer underneath gives users access to governed metrics. Without the semantic layer, self-service users define metrics ad-hoc and the numbers diverge.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I prevent metric chaos in self-service BI?
&lt;/h3&gt;

&lt;p&gt;Define each metric once in a &lt;a href="https://bonnard.dev/glossary/semantic-layer" rel="noopener noreferrer"&gt;semantic layer&lt;/a&gt;. Version the definitions in Git. Review changes in pull requests. Serve the same definitions to every consumer. When everyone queries the same governed definitions, there's nothing to reconcile.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I offer self-service BI for customers in 2026?
&lt;/h3&gt;

&lt;p&gt;Decide where customers ask their questions. A traditional embedded BI tool puts a dashboard in your product. The newer surface is the AI agent: customers ask in Claude or ChatGPT and get a chart back. For that surface, &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; adds a &lt;code&gt;visualize&lt;/code&gt; tool to your MCP server that renders interactive charts from your query results. The two are complementary, not substitutes. Install with &lt;code&gt;npm install @bonnard/mcp-charts&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>semanticlayer</category>
      <category>analytics</category>
      <category>data</category>
      <category>datavisualization</category>
    </item>
    <item>
      <title>Real-Time Analytics: When You Need It and When You Don't</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:25:52 +0000</pubDate>
      <link>https://dev.to/maxbonnard/real-time-analytics-when-you-need-it-and-when-you-dont-2pok</link>
      <guid>https://dev.to/maxbonnard/real-time-analytics-when-you-need-it-and-when-you-dont-2pok</guid>
      <description>&lt;p&gt;"We need real-time analytics" is one of the most common requests in data engineering. It's also one of the most misunderstood. When the VP of Sales says "real-time," they usually mean "faster than the dashboard that refreshes overnight." When the CTO says it, they might mean sub-second event streaming. The gap between those two definitions is a 6-month infrastructure project.&lt;/p&gt;

&lt;p&gt;Most teams don't need true real-time. They need fast enough. And "fast enough" is achievable with pre-aggregation caching at a fraction of the complexity and cost of a streaming architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is real-time analytics?
&lt;/h2&gt;

&lt;p&gt;Real-time analytics means querying data with minimal latency between when an event happens and when it's visible in your analytics. The spectrum:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Freshness&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;True real-time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&amp;lt; 1 second&lt;/td&gt;
&lt;td&gt;Event streaming (Kafka, Flink)&lt;/td&gt;
&lt;td&gt;Fraud detection, stock trading, live monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Near real-time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1-60 seconds&lt;/td&gt;
&lt;td&gt;Micro-batch or streaming&lt;/td&gt;
&lt;td&gt;Operational dashboards, alerting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Frequent refresh&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1-60 minutes&lt;/td&gt;
&lt;td&gt;Scheduled refresh + caching&lt;/td&gt;
&lt;td&gt;KPI dashboards, AI agent queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Batch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hours to daily&lt;/td&gt;
&lt;td&gt;Scheduled ETL&lt;/td&gt;
&lt;td&gt;Board reports, monthly summaries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most analytics use cases fall in the "frequent refresh" category. Revenue by region doesn't need sub-second freshness. Active users in the last hour doesn't need event streaming. A pre-aggregation cache that refreshes every 15 minutes covers 90% of what teams call "real-time."&lt;/p&gt;

&lt;h2&gt;
  
  
  When you actually need real-time
&lt;/h2&gt;

&lt;p&gt;True real-time analytics (sub-second latency from event to query result) is worth the infrastructure investment when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fraud detection.&lt;/strong&gt; Every second of delay is potential fraud that slips through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live monitoring.&lt;/strong&gt; Server health, API error rates, active user counts for live products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trading and pricing.&lt;/strong&gt; Financial instruments where stale data means wrong prices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live events.&lt;/strong&gt; Streaming metrics during a product launch, marketing campaign, or live broadcast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're in one of these categories, you need an event streaming architecture: Kafka, Flink, Materialize, or similar. A semantic layer complements this by governing the metric definitions, but the freshness comes from the streaming pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you don't (and what to use instead)
&lt;/h2&gt;

&lt;p&gt;Most business analytics doesn't need sub-second freshness. It needs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fast query response&lt;/strong&gt; (milliseconds, not seconds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasonably fresh data&lt;/strong&gt; (minutes or hours, not days)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent numbers&lt;/strong&gt; (same answer everywhere)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://bonnard.dev/glossary/pre-aggregation" rel="noopener noreferrer"&gt;Pre-aggregation&lt;/a&gt; gives you all three without a streaming architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  How pre-aggregation works
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://bonnard.dev/glossary/semantic-layer" rel="noopener noreferrer"&gt;semantic layer&lt;/a&gt; pre-computes your most common queries and caches the results. When a dashboard or AI agent asks for "revenue by region this month," the query hits the cache instead of scanning raw warehouse tables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cubes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
    &lt;span class="na"&gt;sql_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public.orders&lt;/span&gt;
    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_revenue&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CASE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;WHEN&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'refunded'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;THEN&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ELSE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;END"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_count&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;time&lt;/span&gt;

    &lt;span class="na"&gt;pre_aggregations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue_by_region&lt;/span&gt;
        &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;total_revenue&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;order_count&lt;/span&gt;
        &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;time_dimension&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;granularity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;day&lt;/span&gt;
        &lt;span class="na"&gt;refresh_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;every&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15 minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cache rebuilds every 15 minutes. Queries that match the pre-aggregation return in single-digit milliseconds. Queries that don't match fall through to the warehouse (seconds).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; your dashboard loads in under a second. Your AI agent gets responses in milliseconds. Your data is at most 15 minutes old. For 90% of analytics use cases, this is indistinguishable from "real-time."&lt;/p&gt;

&lt;h3&gt;
  
  
  Different refresh rates for different metrics
&lt;/h3&gt;

&lt;p&gt;Not all metrics need the same freshness. Configure per rollup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;pre_aggregations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Operational: refresh every 5 minutes&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;active_users_live&lt;/span&gt;
        &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;active_users&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;time_dimension&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;last_seen_at&lt;/span&gt;
        &lt;span class="na"&gt;granularity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;minute&lt;/span&gt;
        &lt;span class="na"&gt;refresh_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;every&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5 minutes&lt;/span&gt;

      &lt;span class="c1"&gt;# Strategic: refresh every hour&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daily_revenue&lt;/span&gt;
        &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;total_revenue&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;mrr&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;time_dimension&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;granularity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;day&lt;/span&gt;
        &lt;span class="na"&gt;refresh_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;every&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1 hour&lt;/span&gt;

      &lt;span class="c1"&gt;# Historical: refresh daily&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monthly_cohorts&lt;/span&gt;
        &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;retention_rate&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;ltv&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;cohort_month&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;time_dimension&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;granularity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;month&lt;/span&gt;
        &lt;span class="na"&gt;refresh_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;every&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;24 hours&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Operational metrics refresh every 5 minutes. Revenue refreshes hourly. Historical cohorts refresh daily. Each metric gets the freshness it needs without over-engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-time analytics for B2B products
&lt;/h2&gt;

&lt;p&gt;If you're shipping analytics to B2B customers, "real-time" has a different meaning. Your customers expect their dashboards to load fast and show recent data. They don't expect sub-second event streaming.&lt;/p&gt;

&lt;p&gt;Pre-aggregation handles this well:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast load times.&lt;/strong&gt; Cached queries return in milliseconds. Customer dashboards feel instant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-tenant caching.&lt;/strong&gt; The cache respects tenant isolation. Customer A's cached rollup doesn't include Customer B's data. Security context filters apply at cache build time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost control.&lt;/strong&gt; On usage-based warehouses (&lt;a href="https://bonnard.dev/integrations/bigquery" rel="noopener noreferrer"&gt;BigQuery&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/snowflake" rel="noopener noreferrer"&gt;Snowflake&lt;/a&gt;), fewer raw queries means lower costs. Serving from cache is essentially free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI agent compatibility.&lt;/strong&gt; Agents make multiple queries per interaction. Pre-aggregation keeps response times low even with agent-scale query volumes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-time analytics architecture compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Freshness&lt;/th&gt;
&lt;th&gt;Query speed&lt;/th&gt;
&lt;th&gt;Complexity&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Event streaming&lt;/strong&gt; (Kafka + Flink)&lt;/td&gt;
&lt;td&gt;Sub-second&lt;/td&gt;
&lt;td&gt;Fast (materialized views)&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Fraud, trading, live monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Streaming DB&lt;/strong&gt; (Materialize, RisingWave)&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Medium-high&lt;/td&gt;
&lt;td&gt;Continuous queries, CDC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Warehouse + pre-aggregation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Milliseconds (cached)&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low-medium&lt;/td&gt;
&lt;td&gt;Dashboards, AI agents, embedded analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Warehouse direct&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minutes-hours (ETL dependent)&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Ad-hoc analysis, batch reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spreadsheet&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;One-off analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most teams, the warehouse + pre-aggregation approach covers the use case. Add streaming only for the metrics that genuinely need sub-second freshness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Once your queries return fast enough, agents become a consumer, and an agent that fetches rows still has to chart them. Add a &lt;code&gt;visualize&lt;/code&gt; tool to your MCP server so it can:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @bonnard/mcp-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runSql&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That registers the &lt;code&gt;visualize&lt;/code&gt; tool on your server. The agent calls it with a query, your &lt;code&gt;runSql&lt;/code&gt; returns the rows (from a cached rollup or the warehouse), and Bonnard renders an interactive chart inside Claude or ChatGPT. Adapters ship for Postgres, BigQuery, Snowflake, Databricks, and DuckDB, or pass your own &lt;code&gt;runSql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Full walkthrough: &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;Add Interactive Charts to Your MCP Server&lt;/a&gt;. For the design choices behind the tool: &lt;a href="https://bonnard.dev/blog/how-bonnard-builds-agent-friendly-mcps" rel="noopener noreferrer"&gt;How Bonnard Builds Agent-Friendly MCPs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Source is on &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is real-time analytics?
&lt;/h3&gt;

&lt;p&gt;Real-time analytics means querying data with minimal delay between when events happen and when they appear in your analytics. True real-time (sub-second) requires event streaming. Near-real-time (minutes) is achievable with pre-aggregation caching on top of a data warehouse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need real-time analytics?
&lt;/h3&gt;

&lt;p&gt;Most business analytics doesn't need sub-second freshness. If your use case is dashboards, AI agent queries, or &lt;a href="https://bonnard.dev/glossary/embedded-analytics" rel="noopener noreferrer"&gt;embedded analytics&lt;/a&gt;, a pre-aggregation cache refreshing every 5-60 minutes provides "real-time" performance at a fraction of the streaming complexity. True real-time is needed for fraud detection, live monitoring, and financial trading.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between real-time and batch analytics?
&lt;/h3&gt;

&lt;p&gt;Batch analytics processes data on a schedule (hourly, daily). Real-time analytics processes data continuously with minimal delay. Pre-aggregation caching sits in between: the cache rebuilds on a schedule (every N minutes), but queries against the cache return in milliseconds. It's batch refresh with real-time query performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does pre-aggregation make analytics feel real-time?
&lt;/h3&gt;

&lt;p&gt;Pre-aggregation pre-computes your most common queries and caches the results. When a user or AI agent queries "revenue by region," the response comes from the cache in single-digit milliseconds instead of scanning raw warehouse tables (which takes seconds). The data is minutes old, but the query feels instant.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is streaming analytics?
&lt;/h3&gt;

&lt;p&gt;Streaming analytics processes events as they arrive, maintaining continuously updated query results. Technologies include Kafka, Flink, Materialize, and RisingWave. It's the right choice for sub-second freshness requirements. For most business analytics, pre-aggregation caching provides comparable user experience at lower cost and complexity.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>database</category>
      <category>data</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>KPI Dashboards Are Broken. Here's What Replaces Them.</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:25:47 +0000</pubDate>
      <link>https://dev.to/maxbonnard/kpi-dashboards-are-broken-heres-what-replaces-them-1f6p</link>
      <guid>https://dev.to/maxbonnard/kpi-dashboards-are-broken-heres-what-replaces-them-1f6p</guid>
      <description>&lt;p&gt;Your company has a KPI dashboard. It was built six months ago by someone who has since moved teams. It shows revenue, churn, and a few product metrics. It loads slowly. The numbers don't match what finance reports. Nobody trusts it, but everyone screenshots it for the Monday standup.&lt;/p&gt;

&lt;p&gt;This is the state of KPI dashboards at most companies. Not because the tools are bad, but because the approach is wrong. A dashboard is a static view of a dynamic system. The moment someone builds it, it starts drifting from reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a KPI dashboard?
&lt;/h2&gt;

&lt;p&gt;A KPI (Key Performance Indicator) dashboard is a visual display of an organization's most important metrics. Revenue, customer count, churn rate, conversion rate, average order value, NPS. The metrics that tell you whether the business is healthy.&lt;/p&gt;

&lt;p&gt;Traditional KPI dashboards live in a BI tool: Power BI, Tableau, Looker, Metabase, Grafana. An analyst builds the dashboard, connects it to a data source, and shares a link. People visit the dashboard (or receive a scheduled screenshot) to check the numbers.&lt;/p&gt;

&lt;p&gt;The concept is sound. The execution breaks for predictable reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  KPI dashboard examples
&lt;/h2&gt;

&lt;p&gt;Before diving into what's broken, here's what teams typically build:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SaaS executive dashboard.&lt;/strong&gt; MRR, ARR, net revenue retention, churn rate, new customers this month, average contract value. Updated daily. Viewed by the CEO and board. The numbers must match the finance team's report exactly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product analytics dashboard.&lt;/strong&gt; Daily active users, feature adoption rates, conversion funnel stages, time to value. Updated hourly. Viewed by product managers. Often the first dashboard built and the first to drift from reality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customer health dashboard (B2B).&lt;/strong&gt; Per-customer usage metrics, support ticket volume, NPS, renewal risk score. Updated daily. Viewed by customer success. In embedded analytics use cases, the customer sees this too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engineering operations dashboard.&lt;/strong&gt; API error rates, p95 latency, deployment frequency, uptime. Updated real-time. Viewed by engineering leads. Often built in Grafana rather than a BI tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sales pipeline dashboard.&lt;/strong&gt; Open deals by stage, weighted pipeline value, win rate, average deal cycle. Updated daily. Viewed by sales leadership. Usually lives in Salesforce or a connected BI tool.&lt;/p&gt;

&lt;p&gt;Each of these follows the same pattern: connect to a data source, write queries, build charts, share a link. The problems start when multiple dashboards need the same metric (revenue, active users, churn) and define it differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why KPI dashboards fail
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The metrics drift
&lt;/h3&gt;

&lt;p&gt;Revenue is defined in the dashboard as &lt;code&gt;SUM(amount) WHERE status = 'completed'&lt;/code&gt;. Finance defines it as &lt;code&gt;SUM(amount) WHERE status != 'refunded' AND type != 'trial'&lt;/code&gt;. The CFO looks at the dashboard, compares it to the finance report, and stops trusting the dashboard. This happens every quarter at companies without a shared metric layer.&lt;/p&gt;

&lt;p&gt;The root cause: business logic is defined in the dashboard, not in a governed layer. Every dashboard that shows revenue re-implements the calculation. Each implementation drifts independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nobody opens them
&lt;/h3&gt;

&lt;p&gt;Dashboards that load in 8 seconds don't get used. Dashboards behind a BI tool login don't get used. Dashboards that require navigating to a separate tool don't get used. The data team builds 50 dashboards. Three get regular traffic. The rest are abandoned.&lt;/p&gt;

&lt;p&gt;The data team keeps building new ones because stakeholders keep asking. Each request means a new dashboard, a new query, a new maintenance burden. The team becomes a dashboard factory instead of building the metrics layer that would make dashboards unnecessary.&lt;/p&gt;

&lt;h3&gt;
  
  
  They only serve one surface
&lt;/h3&gt;

&lt;p&gt;A KPI dashboard in Looker serves people who open Looker. What about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The product team that wants metrics in their React app?&lt;/li&gt;
&lt;li&gt;The customer success team that wants account health in Slack?&lt;/li&gt;
&lt;li&gt;The AI agent that needs to answer "how's revenue trending?"&lt;/li&gt;
&lt;li&gt;The B2B customer that wants their own usage dashboard inside your product?&lt;/li&gt;
&lt;li&gt;The executive who wants a weekly email summary?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these requires a different integration. The dashboard metric definitions don't transfer. You end up rebuilding the same KPIs in every tool, with each copy drifting independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stale data, stale insight
&lt;/h3&gt;

&lt;p&gt;Most KPI dashboards refresh on a schedule: hourly, daily, sometimes manually. Between refreshes, the numbers are stale. For operational KPIs (active users right now, orders this hour, API error rate), stale data is useless data.&lt;/p&gt;

&lt;p&gt;Even with real-time refresh, the dashboard is a passive display. It shows numbers. It doesn't answer questions. "Revenue is down 12% this week" is visible on the dashboard. "Why?" requires a human to open the BI tool, write a query, drill into dimensions, and figure it out. An AI agent could answer that question in seconds, but the dashboard can't feed an agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The alternative: a KPI chart the agent renders on demand
&lt;/h2&gt;

&lt;p&gt;The static dashboard is a snapshot someone built once. The agent path is different: instead of opening a tool and reading a chart someone else laid out, you ask "how's revenue trending?" and the agent runs the query and draws the chart in the conversation.&lt;/p&gt;

&lt;p&gt;That last step (drawing the chart) is what &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; adds: a &lt;code&gt;visualize&lt;/code&gt; tool you add to your MCP server that renders interactive charts from your query results. The agent already has access to your data. The &lt;code&gt;visualize&lt;/code&gt; tool turns the rows it gets back into a chart the person can actually read.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add the visualize tool
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @bonnard/mcp-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// runSql runs against your warehouse and returns typed rows&lt;/span&gt;
&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runSql&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;addCharts&lt;/code&gt; registers a &lt;code&gt;visualize&lt;/code&gt; tool (plus a &lt;code&gt;visualize_read_me&lt;/code&gt; companion) on your MCP server. Bonnard never touches your database. Your &lt;code&gt;runSql&lt;/code&gt; does, against Postgres, BigQuery, Snowflake, Databricks, DuckDB, or any source you wire up.&lt;/p&gt;

&lt;h3&gt;
  
  
  How a KPI request flows
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Someone asks the agent "how's MRR trending this quarter?"&lt;/li&gt;
&lt;li&gt;The agent calls &lt;code&gt;visualize&lt;/code&gt; with a query, and your &lt;code&gt;runSql&lt;/code&gt; returns the rows.&lt;/li&gt;
&lt;li&gt;Bonnard infers the chart from the typed result: a line chart for MRR over time, a bar chart for revenue by plan, a funnel for a conversion sequence.&lt;/li&gt;
&lt;li&gt;Claude or ChatGPT renders an interactive &lt;code&gt;ui://&lt;/code&gt; widget right in the conversation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The chart types cover the KPI cases you'd reach for on a dashboard: line, bar, and area for trends, bar variants (stacked, grouped, horizontal, 100%) for breakdowns, pie for shares, scatter for distributions, funnel and waterfall for staged metrics, and a table when the answer is a table. Axes, number formatting, and gap-fill come from the typed schema, so the same KPI query renders the same chart every time.&lt;/p&gt;

&lt;p&gt;No dashboard to patch. The KPI gets drawn the moment someone asks, from whatever query answers the question.&lt;/p&gt;

&lt;h2&gt;
  
  
  KPI dashboard tools compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Metric governance&lt;/th&gt;
&lt;th&gt;Surfaces&lt;/th&gt;
&lt;th&gt;AI agent support&lt;/th&gt;
&lt;th&gt;Multi-tenant&lt;/th&gt;
&lt;th&gt;Performance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Power BI dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DAX (locked to PBI)&lt;/td&gt;
&lt;td&gt;Power BI only&lt;/td&gt;
&lt;td&gt;Copilot (internal)&lt;/td&gt;
&lt;td&gt;Complex&lt;/td&gt;
&lt;td&gt;Import mode or DirectQuery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tableau dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Semantic model (locked)&lt;/td&gt;
&lt;td&gt;Tableau only&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Extract or live&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Looker dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LookML&lt;/td&gt;
&lt;td&gt;Looker + embed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;PDTs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Metabase dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Metabase + embed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Enterprise only&lt;/td&gt;
&lt;td&gt;No caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grafana dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Grafana only&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;Query-level&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Superset dashboard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Superset + embed&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;No caching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bonnard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inherits your &lt;code&gt;runSql&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Claude and ChatGPT (MCP apps)&lt;/td&gt;
&lt;td&gt;MCP native (&lt;code&gt;visualize&lt;/code&gt; tool)&lt;/td&gt;
&lt;td&gt;Inherits your &lt;code&gt;runSql&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Inherits your warehouse&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The traditional tools render KPIs inside the dashboard tool. Bonnard renders them as interactive charts via the MCP &lt;code&gt;visualize&lt;/code&gt; tool, from query results, inside Claude or ChatGPT.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to keep your KPI dashboard
&lt;/h2&gt;

&lt;p&gt;KPI dashboards aren't always wrong. They work when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One team, one tool, one audience&lt;/li&gt;
&lt;li&gt;The metrics are simple and stable&lt;/li&gt;
&lt;li&gt;Nobody else needs the same numbers in a different format&lt;/li&gt;
&lt;li&gt;You don't need AI agent access or embedded analytics&lt;/li&gt;
&lt;li&gt;The dashboard builder is still on the team and maintains it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They break when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple teams need the same KPIs in different tools&lt;/li&gt;
&lt;li&gt;Customers need to see their metrics inside your product&lt;/li&gt;
&lt;li&gt;AI agents need to query KPIs&lt;/li&gt;
&lt;li&gt;The dashboard shows different numbers than finance reports&lt;/li&gt;
&lt;li&gt;The data team spends more time maintaining dashboards than defining metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Add a &lt;code&gt;visualize&lt;/code&gt; tool to your MCP server so an agent can render a KPI chart on demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @bonnard/mcp-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runSql&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That registers the &lt;code&gt;visualize&lt;/code&gt; tool on your server. Someone asks the agent for a KPI, your &lt;code&gt;runSql&lt;/code&gt; returns the rows, and Bonnard renders an interactive chart inside Claude or ChatGPT. Adapters ship for Postgres, BigQuery, Snowflake, Databricks, and DuckDB, or pass your own &lt;code&gt;runSql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Full walkthrough: &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;Add Interactive Charts to Your MCP Server&lt;/a&gt;. For the design choices behind the tool: &lt;a href="https://bonnard.dev/blog/how-bonnard-builds-agent-friendly-mcps" rel="noopener noreferrer"&gt;How Bonnard Builds Agent-Friendly MCPs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Source is on &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a KPI dashboard?
&lt;/h3&gt;

&lt;p&gt;A KPI dashboard is a visual display of key performance indicators: revenue, churn, active users, conversion rate, and other metrics that measure business health. Traditional KPI dashboards live in BI tools like Power BI, Tableau, Looker, or Grafana. Modern approaches define KPIs in a semantic layer and serve them across dashboards, AI agents, and embedded analytics simultaneously.&lt;/p&gt;

&lt;h3&gt;
  
  
  What KPIs should be on a dashboard?
&lt;/h3&gt;

&lt;p&gt;The KPIs depend on your business. Common B2B SaaS KPIs: MRR, ARR, churn rate, net revenue retention, active users, customer count, NPS, average contract value. The specific metrics matter less than the governance: every KPI should have one canonical definition that every consumer references.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the best KPI dashboard tool?
&lt;/h3&gt;

&lt;p&gt;For internal-only dashboards: Power BI (Microsoft ecosystem), Tableau (Salesforce ecosystem), Looker (Google Cloud), or Metabase (open source). For governed KPIs served to multiple surfaces including AI agents and &lt;a href="https://bonnard.dev/glossary/embedded-analytics" rel="noopener noreferrer"&gt;embedded analytics&lt;/a&gt;: a semantic layer approach. The right choice depends on how many consumers need the same metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make my KPI dashboard load faster?
&lt;/h3&gt;

&lt;p&gt;Most dashboard slowness comes from querying the warehouse on every render. &lt;a href="https://bonnard.dev/glossary/pre-aggregation" rel="noopener noreferrer"&gt;Pre-aggregation&lt;/a&gt; caching pre-computes common KPI rollups. The dashboard queries the cache instead of the warehouse. Response times drop from seconds to single-digit milliseconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can AI agents query KPIs?
&lt;/h3&gt;

&lt;p&gt;With a semantic layer and MCP support, yes. AI agents discover available KPIs via &lt;code&gt;explore_schema&lt;/code&gt; and query them via &lt;code&gt;query&lt;/code&gt;. The agent gets governed, multi-tenant data scoped to their access level. Without a semantic layer, agents generate ad-hoc SQL and return inconsistent numbers. See &lt;a href="https://bonnard.dev/blog/what-is-agentic-semantic-layer" rel="noopener noreferrer"&gt;What Is an Agentic Semantic Layer?&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between a KPI dashboard and analytics dashboard?
&lt;/h3&gt;

&lt;p&gt;A KPI dashboard focuses on a small set of headline metrics that measure business health. An analytics dashboard provides broader exploration: filtering, drilling, and ad-hoc querying. In practice, both benefit from governed metric definitions. The KPI dashboard shows the numbers. The analytics dashboard lets you investigate why they changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should a KPI dashboard refresh?
&lt;/h3&gt;

&lt;p&gt;Depends on the KPI. Revenue and churn: daily or hourly. Active users and API metrics: real-time or near-real-time. Strategic KPIs (NPS, retention cohorts): weekly or monthly. With pre-aggregation, refresh frequency is configurable per metric. Set aggressive refresh for operational KPIs and relaxed refresh for strategic ones.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>datavisualization</category>
      <category>data</category>
      <category>semanticlayer</category>
    </item>
    <item>
      <title>How Bonnard Builds Agent-Friendly MCPs</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:25:44 +0000</pubDate>
      <link>https://dev.to/maxbonnard/how-bonnard-builds-agent-friendly-mcps-n4i</link>
      <guid>https://dev.to/maxbonnard/how-bonnard-builds-agent-friendly-mcps-n4i</guid>
      <description>&lt;p&gt;Exposing your data over MCP is the easy part. Designing a tool an agent uses &lt;em&gt;well&lt;/em&gt; is the hard part. An agent can only use a tool it can read, so the work is shaping the tool for how the model calls it, not just for the human looking at the result. These are the techniques behind &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;&lt;code&gt;@bonnard/mcp-charts&lt;/code&gt;&lt;/a&gt; and the &lt;code&gt;visualize&lt;/code&gt; tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discovery-first, so the agent stops guessing
&lt;/h2&gt;

&lt;p&gt;An agent that guesses your schema writes wrong queries. So the first tool the agent meets is a discovery tool. It calls &lt;code&gt;visualize_read_me&lt;/code&gt; to load the chart options, the tool schema, and worked examples before it ever calls &lt;code&gt;visualize&lt;/code&gt;, and an &lt;code&gt;explore_schema&lt;/code&gt; tool to learn your tables and columns before it writes SQL. The agent reads, then acts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small set of purpose-built tools
&lt;/h2&gt;

&lt;p&gt;The temptation is one tool per metric, or a single tool that takes arbitrary SQL and hopes. Both fail: too many tools blow the agent's attention budget; one firehose tool gives it no guardrails. Bonnard ships a small set, discover, query, visualize, each with a narrow, obvious job. The agent picks the right one because there are few of them and each does one thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// a small, purpose-built set, not one tool per metric&lt;/span&gt;
&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;explore_schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* list tables + columns */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;listSchema&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runSql&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// registers visualize_read_me + visualize&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Compact, honest responses
&lt;/h2&gt;

&lt;p&gt;A tool that returns 10,000 raw rows poisons the context window and the agent's next decision. Bonnard's responses are sized for a model to read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Row caps with a completeness flag.&lt;/strong&gt; Results are capped and tagged &lt;code&gt;partial&lt;/code&gt; or &lt;code&gt;complete&lt;/code&gt;, so the agent knows whether it is looking at everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial-result warnings.&lt;/strong&gt; When results are capped, the response says so and tells the agent not to sum or average the visible rows, use a measure instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summaries over dumps.&lt;/strong&gt; The chart comes back with a compact text summary the model can reason over, not just an image it cannot read.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Errors that guide the next action
&lt;/h2&gt;

&lt;p&gt;A bare "error: invalid column" stalls an agent. Bonnard's errors carry a fix. A bad field name returns a hint to check names with &lt;code&gt;explore_schema&lt;/code&gt;; a SQL error returns targeted guidance; successful calls include a &lt;code&gt;next_step&lt;/code&gt; pointing at the right follow-up tool. The agent self-corrects instead of looping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Determinism over improvisation
&lt;/h2&gt;

&lt;p&gt;The most important move is pushing deterministic work out of the model and into the code. The agent picks &lt;em&gt;what&lt;/em&gt; to show (a chart type, a query). Bonnard decides &lt;em&gt;how&lt;/em&gt;: it infers axes from the typed result, auto-detects currency and percentage formatting, fills missing time buckets, and labels null dimensions. The smaller the agent's decision surface, the less there is to hallucinate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool descriptions that double as instructions
&lt;/h2&gt;

&lt;p&gt;To an agent, the tool description &lt;em&gt;is&lt;/em&gt; the documentation. Bonnard's tools use a typed schema with a description on every field, plus a read-me tool that returns usage examples on demand. The agent knows how and when to call &lt;code&gt;visualize&lt;/code&gt; because the schema tells it, not because you wrote a prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What makes an MCP tool "agent-friendly"?
&lt;/h3&gt;

&lt;p&gt;It is shaped for how a model calls tools: discovery-first so the agent learns before it acts, a small set of narrow tools, compact responses with completeness flags, errors that suggest the fix, and a typed, self-describing schema. The agent uses it correctly without hand-holding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why not just let the agent write SQL and draw the chart?
&lt;/h3&gt;

&lt;p&gt;Text-to-SQL against raw tables is wrong often enough that you cannot put it in front of a customer, and an agent drawing its own chart HTML is non-deterministic. A governed query path plus a dedicated chart tool returns consistent, trustworthy visuals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Bonnard support raw SQL at all?
&lt;/h3&gt;

&lt;p&gt;Yes, as a narrow, audited path, not a bypass around governance. You control what runs via your &lt;code&gt;runSql&lt;/code&gt; callback.&lt;/p&gt;

&lt;p&gt;See the product this is built on: &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP Charts&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>agentexperience</category>
      <category>charts</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>How to Build Customer-Facing Analytics for B2B SaaS</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:25:42 +0000</pubDate>
      <link>https://dev.to/maxbonnard/how-to-build-customer-facing-analytics-for-b2b-saas-h91</link>
      <guid>https://dev.to/maxbonnard/how-to-build-customer-facing-analytics-for-b2b-saas-h91</guid>
      <description>&lt;p&gt;Your B2B customers want to see their data. Usage metrics, billing summaries, conversion funnels, performance dashboards. Every customer expects analytics inside your product. They shouldn't have to ask your support team for a CSV export.&lt;/p&gt;

&lt;p&gt;The question isn't whether to ship customer-facing analytics. It's how.&lt;/p&gt;

&lt;p&gt;Most teams start with one of two approaches. They embed a BI tool (Metabase, Looker, Power BI) and fight with multi-tenancy, iframe styling, and paid embedding licenses. Or they build custom charts from scratch and spend months maintaining SQL queries, API endpoints, and frontend components that nobody asked for.&lt;/p&gt;

&lt;p&gt;Both approaches burn engineering time on the wrong problem. You end up building analytics infrastructure instead of your product.&lt;/p&gt;

&lt;p&gt;And there's a surface most of these tools miss entirely: the AI agent. Customers increasingly want to ask questions about their data inside Claude or ChatGPT and get a chart back. That's a different problem from embedding a dashboard, and it's where &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; fits, covered later in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why embedded BI tools fall short
&lt;/h2&gt;

&lt;p&gt;Embedding a BI tool sounds fast. Drop in an iframe, connect to your database, ship it. In practice, the friction shows up quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-tenancy is an afterthought
&lt;/h3&gt;

&lt;p&gt;Most BI tools were built for internal teams, not B2B products serving hundreds of tenants. Multi-tenancy is either missing, manual, or gated behind an enterprise license.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bonnard.dev/vs-metabase" rel="noopener noreferrer"&gt;Metabase&lt;/a&gt; requires the Enterprise license ($500+/month) for row-level permissions and sandboxed embedding. The open-source version has basic embedding but no tenant isolation. You end up writing middleware to filter queries by tenant, which is exactly the custom infrastructure you were trying to avoid.&lt;/p&gt;

&lt;p&gt;Looker's embedded analytics requires an enterprise contract. Power BI Embedded uses capacity-based pricing that gets expensive at scale. Tableau's embedding story is Salesforce-priced.&lt;/p&gt;

&lt;p&gt;Even when multi-tenancy is available, it's usually dashboard-level or role-based, not per-query structural enforcement. You're trusting the BI tool to filter correctly on every query. One misconfiguration and Customer A sees Customer B's data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Styling and UX limitations
&lt;/h3&gt;

&lt;p&gt;An iframe is a foreign element in your product. It looks like a foreign element. Matching your brand's fonts, colors, spacing, and interaction patterns inside an embedded BI tool ranges from difficult to impossible. Your customers notice.&lt;/p&gt;

&lt;p&gt;White-label analytics means your customers shouldn't know they're using a third-party tool. Most embedded BI solutions make this hard. The ones that make it easy charge for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  You're locked to dashboards
&lt;/h3&gt;

&lt;p&gt;Embedded BI gives you dashboards. That's one surface. But your customers might also want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An API to pull metrics into their own tools&lt;/li&gt;
&lt;li&gt;AI agents that answer questions about their data&lt;/li&gt;
&lt;li&gt;Scheduled email reports&lt;/li&gt;
&lt;li&gt;Webhook alerts when metrics cross thresholds&lt;/li&gt;
&lt;li&gt;CSV/Excel exports for their finance team&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these requires a different integration, often with different tools and different metric definitions. The dashboard shows one number. The API returns a different one. The export uses yet another query. The metrics drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why building from scratch is worse
&lt;/h2&gt;

&lt;p&gt;The alternative: skip the BI tool and build it yourself. Custom SQL queries, custom API endpoints, custom React charts.&lt;/p&gt;

&lt;p&gt;This works for the first dashboard. Then the second. By the tenth, you're maintaining a bespoke analytics platform. Every new metric means a new SQL query, a new API endpoint, a new frontend component, and a new set of tests. Your data engineers are writing API handlers instead of defining metrics. Your frontend engineers are debugging chart edge cases instead of building product features.&lt;/p&gt;

&lt;p&gt;The real cost isn't the initial build. It's the ongoing maintenance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metric drift.&lt;/strong&gt; Revenue is calculated differently in the dashboard, the API, and the export. Nobody notices until a customer complains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No caching layer.&lt;/strong&gt; Every API call hits the warehouse. Response times grow with data volume. Your customers experience slow dashboards at month-end when they need them most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security is DIY.&lt;/strong&gt; You build tenant filtering yourself. You test it yourself. You hope you didn't miss an edge case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No schema evolution.&lt;/strong&gt; Adding a new dimension means updating every query, endpoint, and component that touches the affected metric. A one-line change in business logic cascades into a multi-day project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The teams on Reddit asking &lt;a href="https://www.reddit.com/r/analytics/comments/1pxjm1z/" rel="noopener noreferrer"&gt;"Is embedded analytics for SaaS actually worth it vs building your own charts?"&lt;/a&gt; are wrestling with exactly this tradeoff. There's no single right answer. The decision depends on how many surfaces you serve and whether your customers want charts in a dashboard, an API, or an AI agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI agent surface: charts inside Claude and ChatGPT
&lt;/h2&gt;

&lt;p&gt;The sections above cover the two classic customer-facing analytics options: embed a BI tool or build dashboards from scratch. There's a third surface that neither covers well: the AI agent. When your customer asks a question in Claude or ChatGPT and wants a chart back, an embedded dashboard doesn't help.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; is the MCP-native option for that surface. It is not an embedded BI tool, a dashboard product, or a multi-tenant analytics platform. It is a &lt;code&gt;visualize&lt;/code&gt; tool you add to your MCP server that renders interactive charts from your query results, inside the agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;Install the package and call &lt;code&gt;addCharts&lt;/code&gt; with your existing MCP server and a &lt;code&gt;runSql&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runSql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Run the query against your database and return typed rows.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;addCharts&lt;/code&gt; registers a &lt;code&gt;visualize&lt;/code&gt; tool (and a &lt;code&gt;visualize_read_me&lt;/code&gt; companion) on your server. The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent calls &lt;code&gt;visualize&lt;/code&gt; with a query.&lt;/li&gt;
&lt;li&gt;Your &lt;code&gt;runSql&lt;/code&gt; returns rows.&lt;/li&gt;
&lt;li&gt;Bonnard infers the chart from the typed result.&lt;/li&gt;
&lt;li&gt;It renders an interactive &lt;code&gt;ui://&lt;/code&gt; widget in Claude or ChatGPT.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Chart types: line, bar, area, pie, scatter, funnel, waterfall, and table. Bar charts have stacked, grouped, horizontal, and 100% variants, and a donut is a pie variant. Axes, formatting, and gap-fill are determined automatically, so the same query produces the same chart.&lt;/p&gt;

&lt;p&gt;Native adapters cover Postgres, BigQuery, Snowflake, Databricks, and DuckDB. You can also pass your own &lt;code&gt;runSql&lt;/code&gt;. Bonnard never touches your database. Your code runs the query and returns the rows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Built for the agent, not only the human
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;visualize&lt;/code&gt; tool returns compact summaries, row caps with completeness flags, and a typed schema so the agent can reason about the result. Errors are instructive and include a &lt;code&gt;next_step&lt;/code&gt;, so when a query fails the agent knows what to try next. For how this is designed, see &lt;a href="https://bonnard.dev/blog/how-bonnard-builds-agent-friendly-mcps" rel="noopener noreferrer"&gt;how Bonnard builds agent-friendly MCPs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build vs. embed vs. charts in the agent
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Build from scratch&lt;/th&gt;
&lt;th&gt;Embed BI tool&lt;/th&gt;
&lt;th&gt;Charts in AI agent (MCP)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Surface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In your product UI&lt;/td&gt;
&lt;td&gt;In your product UI&lt;/td&gt;
&lt;td&gt;Inside Claude or ChatGPT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time to first chart&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2-4 months&lt;/td&gt;
&lt;td&gt;1-2 weeks&lt;/td&gt;
&lt;td&gt;A few lines (&lt;code&gt;addCharts&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What renders the chart&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your frontend code&lt;/td&gt;
&lt;td&gt;The BI tool&lt;/td&gt;
&lt;td&gt;The agent host (&lt;code&gt;ui://&lt;/code&gt; widget)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Where the query runs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your code&lt;/td&gt;
&lt;td&gt;The BI tool&lt;/td&gt;
&lt;td&gt;Your &lt;code&gt;runSql&lt;/code&gt;, your database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chart types&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Whatever you build&lt;/td&gt;
&lt;td&gt;Tool's library&lt;/td&gt;
&lt;td&gt;line, bar, area, pie, scatter, funnel, waterfall, table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance burden&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (you own everything)&lt;/td&gt;
&lt;td&gt;Medium (tool updates, iframes)&lt;/td&gt;
&lt;td&gt;Low (one tool registration)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI agent support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Build from scratch&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Native (&lt;code&gt;visualize&lt;/code&gt; tool)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are not competing for the same slot. Embedded BI and a from-scratch build put charts in your product UI. Charts in the agent put them inside Claude or ChatGPT. Many teams will do both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes the AI agent surface different
&lt;/h2&gt;

&lt;p&gt;Embedded analytics tools (Metabase Embedded, Holistics, Explo, Luzmo, Reveal) give you dashboards inside your product. That's their scope. They're good at it. None of them render a chart inside an AI agent.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; is the adjacent option, not a member of the embedded BI category:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The chart lives in the agent.&lt;/strong&gt; When a customer asks a question in Claude or ChatGPT, the &lt;code&gt;visualize&lt;/code&gt; tool renders an interactive &lt;code&gt;ui://&lt;/code&gt; widget right in the conversation. An embedded dashboard can't do that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Your database stays yours.&lt;/strong&gt; Bonnard never touches your database. Your &lt;code&gt;runSql&lt;/code&gt; runs the query and returns the rows. Bonnard infers the chart from the typed result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Designed for the agent.&lt;/strong&gt; Compact summaries, row caps with completeness flags, typed schema, and instructive errors with a &lt;code&gt;next_step&lt;/code&gt;. The tool is built for an agent to call, not only for a human to read.&lt;/p&gt;

&lt;p&gt;The tradeoff: embedded BI tools render charts in your product UI, which is where many customers expect them. &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; only covers the agent surface. If your customers want both a dashboard and an in-agent chart, use an embedded BI tool for the dashboard and &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; for the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Install the package and register the tool on your MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @bonnard/mcp-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runSql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That registers the &lt;code&gt;visualize&lt;/code&gt; tool (and &lt;code&gt;visualize_read_me&lt;/code&gt;). Connect your server in Claude or ChatGPT, and the agent can render charts from your query results. Native adapters cover Postgres, BigQuery, Snowflake, Databricks, and DuckDB, or pass your own &lt;code&gt;runSql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;github.com/bonnard-data/mcp-charts&lt;/a&gt;. For the full walkthrough: &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP charts&lt;/a&gt;. For the design principles: &lt;a href="https://bonnard.dev/blog/how-bonnard-builds-agent-friendly-mcps" rel="noopener noreferrer"&gt;how Bonnard builds agent-friendly MCPs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is customer-facing analytics?
&lt;/h3&gt;

&lt;p&gt;Customer-facing analytics is analytics embedded in your product for your customers to use. Instead of internal dashboards for your team, the analytics are exposed to end users: your B2B customers, their teams, and their tools. The key challenges are multi-tenancy (each customer sees only their data), performance (customers expect fast load times), and consistency (the numbers should match across every surface).&lt;/p&gt;

&lt;h3&gt;
  
  
  What is embedded analytics?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://bonnard.dev/glossary/embedded-analytics" rel="noopener noreferrer"&gt;Embedded analytics&lt;/a&gt; means integrating analytics capabilities directly into another application. This can be as simple as an iframe embedding a dashboard or as sophisticated as native React components querying a governed API. The term covers a range of approaches from basic chart embedding to full white-label analytics platforms.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is this different from Metabase embedding?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://bonnard.dev/vs-metabase" rel="noopener noreferrer"&gt;Metabase&lt;/a&gt; offers embedded dashboards via iframe or full-app embedding. The open-source version has basic embedding but no tenant isolation. The Enterprise version ($500+/month) adds row-level permissions and sandboxed embedding. Metabase renders charts in your product UI. &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; is a different surface entirely: it renders interactive charts inside an AI agent (Claude or ChatGPT) via a &lt;code&gt;visualize&lt;/code&gt; tool on your MCP server. They are not substitutes. Use Metabase for an in-product dashboard, and &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; for charts in the agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is white-label analytics?
&lt;/h3&gt;

&lt;p&gt;White-label analytics means your customers see your brand, not a third-party tool's brand. No "Powered by Metabase" footer. No foreign-looking iframe. The analytics feel native to your product. Most embedded BI tools charge for white-labeling, and the harder it is to match your design, the more obvious the third-party tool becomes. This is a property of in-product embedding, not of charts rendered inside an AI agent.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is customer-facing analytics for AI agents?
&lt;/h3&gt;

&lt;p&gt;It is letting your customers ask questions about their data inside an AI agent (Claude or ChatGPT) and get a chart back. &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; covers this with a &lt;code&gt;visualize&lt;/code&gt; tool you add to your MCP server. The agent calls &lt;code&gt;visualize&lt;/code&gt;, your &lt;code&gt;runSql&lt;/code&gt; returns rows, and Bonnard renders an interactive chart in the conversation. This is the 2026 surface that embedded BI tools do not address.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can my customers get charts inside an AI agent?
&lt;/h3&gt;

&lt;p&gt;Yes, if you expose an MCP server. Add &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; with &lt;code&gt;addCharts(server, { runSql })&lt;/code&gt;, and the agent can call &lt;code&gt;visualize&lt;/code&gt; to render interactive charts from your query results in Claude or ChatGPT. Your &lt;code&gt;runSql&lt;/code&gt; runs the query against your database, so Bonnard never touches your data. See &lt;a href="https://bonnard.dev/glossary/mcp" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; for how the protocol works.&lt;/p&gt;

&lt;h3&gt;
  
  
  What databases does @bonnard/mcp-charts work with?
&lt;/h3&gt;

&lt;p&gt;Native adapters cover &lt;a href="https://bonnard.dev/integrations/postgres" rel="noopener noreferrer"&gt;Postgres&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/bigquery" rel="noopener noreferrer"&gt;BigQuery&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/snowflake" rel="noopener noreferrer"&gt;Snowflake&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/databricks" rel="noopener noreferrer"&gt;Databricks&lt;/a&gt;, and &lt;a href="https://bonnard.dev/integrations/duckdb" rel="noopener noreferrer"&gt;DuckDB&lt;/a&gt;. You can also pass your own &lt;code&gt;runSql&lt;/code&gt; function to query anything that returns rows. Bonnard never connects to your database directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does @bonnard/mcp-charts cost?
&lt;/h3&gt;

&lt;p&gt;It is open source. Install it with &lt;code&gt;npm install @bonnard/mcp-charts&lt;/code&gt;. Repo: &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;github.com/bonnard-data/mcp-charts&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>saas</category>
      <category>data</category>
      <category>datavisualization</category>
    </item>
    <item>
      <title>How to Connect an AI Agent to Your Data Warehouse</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:25:39 +0000</pubDate>
      <link>https://dev.to/maxbonnard/how-to-connect-an-ai-agent-to-your-data-warehouse-ack</link>
      <guid>https://dev.to/maxbonnard/how-to-connect-an-ai-agent-to-your-data-warehouse-ack</guid>
      <description>&lt;p&gt;Most teams connecting AI agents to their data warehouse start with text-to-SQL. The agent generates SQL from natural language, runs it against the warehouse, and returns results. It works until it doesn't: hallucinated JOINs, inconsistent aggregations, no access control, no audit trail.&lt;/p&gt;

&lt;p&gt;There's a better approach. Define your business metrics in a semantic layer, expose them via &lt;a href="https://bonnard.dev/glossary/mcp" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; (Model Context Protocol), and let any AI agent query governed definitions instead of raw tables. Then add one tool so the agent can chart the result in Claude or ChatGPT. This tutorial shows how to set it up in under 30 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does text-to-SQL break in production?
&lt;/h2&gt;

&lt;p&gt;The agent sees column names but not business logic. It doesn't know that your company excludes refunds from revenue. It doesn't know that &lt;code&gt;status = 'completed'&lt;/code&gt; means something different in &lt;code&gt;orders&lt;/code&gt; than in &lt;code&gt;subscriptions&lt;/code&gt;. It doesn't know that marketing and finance defined "active user" differently three years ago and never reconciled.&lt;/p&gt;

&lt;p&gt;So the agent writes plausible SQL and returns plausible numbers. Ask the same question twice with different phrasing and you get different answers. Ask two different agents and you get two different numbers. Neither matches the number your finance team reports.&lt;/p&gt;

&lt;p&gt;Beyond consistency, there's no row-level security. No multi-tenancy. No audit trail showing which agent queried what, when, and for whom. In production, with real customers, that's a non-starter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bonnard.dev/glossary/text-to-sql" rel="noopener noreferrer"&gt;Text-to-SQL&lt;/a&gt; gives you speed. It doesn't give you trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the semantic layer approach?
&lt;/h2&gt;

&lt;p&gt;Instead of letting agents write arbitrary SQL, define your metrics once in YAML: cubes, measures, dimensions, access rules. Then expose those definitions via MCP so agents query governed metrics, not raw tables.&lt;/p&gt;

&lt;p&gt;The difference: every agent gets the same answer because the metric definition is fixed. &lt;code&gt;total_revenue&lt;/code&gt; isn't a column the agent interprets. It's a pre-defined calculation with agreed-upon filters and aggregations. When your finance team updates the revenue definition to exclude trial conversions, that change propagates to every consumer instantly. No agent retrained. No dashboard patched. One diff in your schema repo.&lt;/p&gt;

&lt;p&gt;This architecture also decouples the query interface from the warehouse dialect. Swap BigQuery for Snowflake and your agents don't notice. The semantic layer abstracts the SQL generation, so consumers stay stable while infrastructure evolves underneath.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Governance&lt;/th&gt;
&lt;th&gt;Consistency&lt;/th&gt;
&lt;th&gt;Multi-tenant&lt;/th&gt;
&lt;th&gt;Access Control&lt;/th&gt;
&lt;th&gt;Setup Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Direct SQL&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Varies by query&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-SQL&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Varies by prompt&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic Layer via MCP&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Guaranteed&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Row-level&lt;/td&gt;
&lt;td&gt;Under 30 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;a href="https://bonnard.dev/semantic-layer" rel="noopener noreferrer"&gt;semantic layer&lt;/a&gt; is the control plane between your warehouse and every consumer, whether that's a human analyst, a React component, or an AI agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Set up your semantic layer
&lt;/h2&gt;

&lt;p&gt;Stand up a semantic layer that connects to your warehouse and exposes metrics over MCP. Open-source engines like &lt;a href="https://cube.dev" rel="noopener noreferrer"&gt;Cube&lt;/a&gt; connect to &lt;a href="https://bonnard.dev/integrations/bigquery" rel="noopener noreferrer"&gt;BigQuery&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/snowflake" rel="noopener noreferrer"&gt;Snowflake&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/redshift" rel="noopener noreferrer"&gt;Redshift&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/databricks" rel="noopener noreferrer"&gt;Databricks&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/postgres" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt; (including Supabase, Neon, and RDS), and &lt;a href="https://bonnard.dev/integrations/duckdb" rel="noopener noreferrer"&gt;DuckDB&lt;/a&gt; (including MotherDuck). You configure your warehouse connection in a config file or pass credentials via environment variables.&lt;/p&gt;

&lt;p&gt;If you want to explore without connecting your own warehouse, point the semantic layer at a sample PostgreSQL database with orders, customers, and products data so you can follow along with the rest of this tutorial. A few thousand rows across three tables is enough to test aggregations, filters, and multi-dimensional queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Define your metrics
&lt;/h2&gt;

&lt;p&gt;Create a cube that maps to a table in your warehouse and defines the metrics your agents will query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cubes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
    &lt;span class="na"&gt;sql_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public.orders&lt;/span&gt;
    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_revenue&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amount&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Measures&lt;/strong&gt; are the numbers you aggregate: sums, counts, averages. &lt;strong&gt;Dimensions&lt;/strong&gt; are the columns you filter and group by: status, date, category. One definition. Every tool, dashboard, and AI agent that queries &lt;code&gt;total_revenue&lt;/code&gt; gets the same number.&lt;/p&gt;

&lt;p&gt;You can also define &lt;code&gt;pre_aggregations&lt;/code&gt; in the same file to cache expensive computations. For example, a daily rollup of &lt;code&gt;total_revenue&lt;/code&gt; by &lt;code&gt;status&lt;/code&gt; can cut query times from seconds to single-digit milliseconds. The semantic layer rebuilds these rollups on a configurable schedule and invalidates stale caches automatically.&lt;/p&gt;

&lt;p&gt;Schemas are version-controlled alongside your application code. Review metric changes in pull requests. Roll back a bad definition with &lt;code&gt;git revert&lt;/code&gt;. Your data contracts get the same CI/CD workflow as your product.&lt;/p&gt;

&lt;p&gt;To control what's exposed to specific consumers, define a view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;views&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_metrics&lt;/span&gt;
    &lt;span class="na"&gt;cubes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;join_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
        &lt;span class="na"&gt;includes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;total_revenue&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Views act as a curated interface. Your agents see &lt;code&gt;order_metrics&lt;/code&gt; with four fields instead of navigating the full schema.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Deploy and connect your agent
&lt;/h2&gt;

&lt;p&gt;Deploy your schema so the semantic layer serves it over an MCP endpoint, then add the MCP server URL to your client's config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"semantic-layer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-semantic-layer.example.com/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste this into your MCP client's config file and restart. For Claude Desktop, that's &lt;code&gt;~/Library/Application Support/Claude/claude_desktop_config.json&lt;/code&gt;. For Cursor, it's &lt;code&gt;.cursor/mcp.json&lt;/code&gt; in your project root. Claude Code reads from &lt;code&gt;.mcp.json&lt;/code&gt; in your project directory.&lt;/p&gt;

&lt;p&gt;The MCP server handles tool discovery, schema introspection, and query execution over HTTP. Your agent sees available metrics the same way it sees any other MCP tool. No custom integration code required.&lt;/p&gt;

&lt;p&gt;For customer-facing use cases, scope each connection to a specific tenant's data with row-level security defined in the schema, so a customer's agent sees only that customer's rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Query from your agent
&lt;/h2&gt;

&lt;p&gt;Once connected, your AI agent can discover and query your metrics using natural language. Behind the scenes, the agent calls MCP tools.&lt;/p&gt;

&lt;p&gt;Ask: "What's our total revenue this quarter?"&lt;/p&gt;

&lt;p&gt;The agent calls &lt;code&gt;explore_schema&lt;/code&gt; to discover available metrics, then calls &lt;code&gt;query&lt;/code&gt; with the right measures and time filters. The response comes back as structured data, not raw SQL results.&lt;/p&gt;

&lt;p&gt;Ask: "Break down order count by status for the last 30 days."&lt;/p&gt;

&lt;p&gt;Same flow. The agent uses &lt;code&gt;query&lt;/code&gt; with &lt;code&gt;count&lt;/code&gt; as the measure, &lt;code&gt;status&lt;/code&gt; as the dimension, and a date filter on &lt;code&gt;created_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A semantic layer typically exposes a handful of MCP tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;explore_schema&lt;/code&gt;&lt;/strong&gt;: Discover available cubes, measures, and dimensions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;query&lt;/code&gt;&lt;/strong&gt;: Fetch aggregated data using governed metric definitions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sql_query&lt;/code&gt;&lt;/strong&gt;: Run queries for cases that need custom SQL (still governed by access controls)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;describe_field&lt;/code&gt;&lt;/strong&gt;: Get metadata about a specific measure or dimension&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every query runs through the semantic layer. The agent never touches raw tables. Read more about the architecture in our &lt;a href="https://bonnard.dev/agentic-analytics" rel="noopener noreferrer"&gt;agentic analytics&lt;/a&gt; guide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Charting the agent's results
&lt;/h2&gt;

&lt;p&gt;A governed query returns rows. The agent usually needs to show the user a chart, not a wall of numbers. Leaving the model to draw HTML puts the most important part of the answer in its hands to improvise.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@bonnard/mcp-charts" rel="noopener noreferrer"&gt;&lt;code&gt;@bonnard/mcp-charts&lt;/code&gt;&lt;/a&gt; fixes that. It adds a &lt;code&gt;visualize&lt;/code&gt; tool to your MCP server in a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @bonnard/mcp-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// your data, your connection. Bonnard never touches the database&lt;/span&gt;
&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runSql&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent calls &lt;code&gt;visualize&lt;/code&gt; with a query, your &lt;code&gt;runSql&lt;/code&gt; returns the rows, and Bonnard infers the chart from the typed result, then renders an interactive widget in Claude or ChatGPT. It renders line, bar, area, pie, scatter, funnel, waterfall, and table, with bar variants for stacked, grouped, horizontal, and 100% stacked. The chart comes from your query result, not from tokens the model invents. Same data, same chart, every time. Full walkthrough: &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP Charts&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  From raw SQL to governed metrics
&lt;/h2&gt;

&lt;p&gt;You started with a warehouse and an AI agent that writes its own SQL. Now you have governed metrics defined in YAML, exposed via MCP, queryable from any AI tool your team or customers use. Setup took under 30 minutes.&lt;/p&gt;

&lt;p&gt;The shift from raw SQL to governed metrics pays off immediately: consistent numbers across every consumer, row-level access control per tenant, and a full audit trail for every query. As your team adds more agents and surfaces, the semantic layer scales with you. No duplicate logic. No drift between dashboards and AI answers.&lt;/p&gt;

&lt;p&gt;Give the agent a way to chart what it queries: &lt;code&gt;npm install @bonnard/mcp-charts&lt;/code&gt;, call &lt;code&gt;addCharts(server, { runSql })&lt;/code&gt;, and interactive charts render in Claude and ChatGPT. The repo is on &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Read the &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP Charts guide&lt;/a&gt; to go deeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://bonnard.dev/glossary/mcp" rel="noopener noreferrer"&gt;What is MCP?&lt;/a&gt; -- the protocol that powers agent-to-data connections&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP Charts&lt;/a&gt; -- add a &lt;code&gt;visualize&lt;/code&gt; tool so agents chart query results in Claude and ChatGPT&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://bonnard.dev/integrations/snowflake" rel="noopener noreferrer"&gt;Snowflake integration&lt;/a&gt; -- connect to your Snowflake warehouse&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need to retrain my AI model?
&lt;/h3&gt;

&lt;p&gt;No. A semantic layer with MCP support works with any MCP-compatible AI agent out of the box. Your agent discovers available metrics through the MCP protocol at runtime. There is no fine-tuning, prompt engineering, or model modification required.&lt;/p&gt;

&lt;h3&gt;
  
  
  What data warehouses does this work with?
&lt;/h3&gt;

&lt;p&gt;A semantic layer connects to BigQuery, Snowflake, Redshift, Databricks, PostgreSQL (including Supabase, Neon, and RDS), and DuckDB (including MotherDuck). For charting the agent's results, &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; ships native adapters for Postgres, BigQuery, Snowflake, Databricks, and DuckDB, or you pass your own &lt;code&gt;runSql&lt;/code&gt;. Bonnard never touches your database.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is this different from text-to-SQL?
&lt;/h3&gt;

&lt;p&gt;Text-to-SQL lets an AI agent generate arbitrary SQL from natural language. It has no governance, no consistency guarantees, and no access control. A semantic layer defines metrics once in YAML and exposes them as governed APIs. Every consumer gets the same answer because the calculation is fixed, not interpreted per query. And when the agent charts the result, &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; renders from the query result, not from tokens the model invents.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I let the agent show charts?
&lt;/h3&gt;

&lt;p&gt;Install &lt;a href="https://www.npmjs.com/package/@bonnard/mcp-charts" rel="noopener noreferrer"&gt;&lt;code&gt;@bonnard/mcp-charts&lt;/code&gt;&lt;/a&gt; and call &lt;code&gt;addCharts(server, { runSql })&lt;/code&gt; on your MCP server. That registers a &lt;code&gt;visualize&lt;/code&gt; tool. The agent calls it with a query, your callback returns the rows, and an interactive chart renders in Claude or ChatGPT. No frontend code.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does setup take?
&lt;/h3&gt;

&lt;p&gt;Under 30 minutes for most teams. Stand up a semantic layer, connect your warehouse, define a few metrics in YAML, and deploy it over MCP. Add charts with &lt;code&gt;npm install @bonnard/mcp-charts&lt;/code&gt; and one &lt;code&gt;addCharts&lt;/code&gt; call. The tutorial above walks through each step with working code examples.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>aiagents</category>
      <category>database</category>
      <category>data</category>
    </item>
    <item>
      <title>Best Embedded Analytics Tools for SaaS in 2026</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:25:36 +0000</pubDate>
      <link>https://dev.to/maxbonnard/best-embedded-analytics-tools-for-saas-in-2026-5d0o</link>
      <guid>https://dev.to/maxbonnard/best-embedded-analytics-tools-for-saas-in-2026-5d0o</guid>
      <description>&lt;p&gt;If you're building a B2B SaaS product and need to ship analytics to your customers, you're evaluating embedded analytics tools. The market has more options than it needs. This guide cuts through the noise.&lt;/p&gt;

&lt;p&gt;We compare 10 tools across the dimensions that matter for B2B: multi-tenancy, customization, AI agent support, pricing model, and how many surfaces each tool serves beyond dashboards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for in embedded analytics
&lt;/h2&gt;

&lt;p&gt;Before comparing tools, know what matters for your use case:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenancy.&lt;/strong&gt; Every customer sees only their data. This needs to be structural (enforced at the query layer), not bolted on (middleware filters that might miss an edge case). One data leak and your customers' trust is gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customization.&lt;/strong&gt; The analytics should look like part of your product. Iframe embedding limits this. Native SDK components give you full control over styling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metric governance.&lt;/strong&gt; Every chart should show the same number as your API. If the dashboard says $45K and the API returns $43K, your customer notices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance.&lt;/strong&gt; Dashboards that take 8 seconds to load don't get used. Caching matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing model.&lt;/strong&gt; Per-user pricing kills B2B use cases. If you have 1,000 customers each with 10 users, per-seat licensing makes embedded analytics unaffordable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI agent support.&lt;/strong&gt; Increasingly, customers want to connect AI tools to their data. None of the traditional embedded analytics tools support this. It's a differentiator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 10 tools compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Multi-tenancy&lt;/th&gt;
&lt;th&gt;AI/Agent&lt;/th&gt;
&lt;th&gt;Pricing&lt;/th&gt;
&lt;th&gt;Open source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://bonnard.dev/vs-metabase" rel="noopener noreferrer"&gt;Metabase&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BI tool with embedding&lt;/td&gt;
&lt;td&gt;Enterprise only ($500+/mo)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Free OSS / $500+ Pro&lt;/td&gt;
&lt;td&gt;Yes (AGPL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Holistics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Embedded BI platform&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Per-user&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Explo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Embedded analytics SDK&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Usage-based&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Luzmo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Embedded analytics SDK&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Usage-based&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reveal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Embedded analytics SDK&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Per-app&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GoodData&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enterprise embedded BI&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Usage-based&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://bonnard.dev/vs-looker" rel="noopener noreferrer"&gt;Looker&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enterprise BI with embedding&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Enterprise pricing&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://bonnard.dev/vs-power-bi" rel="noopener noreferrer"&gt;Power BI Embedded&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enterprise BI with embedding&lt;/td&gt;
&lt;td&gt;Complex setup&lt;/td&gt;
&lt;td&gt;Copilot (internal)&lt;/td&gt;
&lt;td&gt;Capacity-based&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://bonnard.dev/vs-tableau" rel="noopener noreferrer"&gt;Tableau Embedded&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enterprise BI with embedding&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Limited&lt;/td&gt;
&lt;td&gt;Per-user + Server&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://bonnard.dev" rel="noopener noreferrer"&gt;Bonnard&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Charts for AI agents via MCP (not embedded BI)&lt;/td&gt;
&lt;td&gt;N/A (your &lt;code&gt;runSql&lt;/code&gt;, your DB)&lt;/td&gt;
&lt;td&gt;Native (&lt;code&gt;visualize&lt;/code&gt; tool)&lt;/td&gt;
&lt;td&gt;Open source (npm)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Tool-by-tool breakdown
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Metabase
&lt;/h3&gt;

&lt;p&gt;The most popular open-source BI tool. Great for internal dashboards. Embedding requires Pro ($500+/mo) for static embeds or Enterprise for interactive. Multi-tenancy (sandboxing) is Enterprise only. No semantic layer, no AI agent support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Internal dashboards, MVPs, teams that want free self-hosted BI.&lt;br&gt;
&lt;strong&gt;Not great for:&lt;/strong&gt; Customer-facing B2B analytics at scale. Multi-tenancy and embedding costs add up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Holistics
&lt;/h3&gt;

&lt;p&gt;Self-service embedded BI aimed at product teams. Built-in multi-tenancy with row-level security. Strong data modeling layer. Drag-and-drop dashboard builder with embedding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Teams that want a managed embedded BI platform with a modeling layer.&lt;br&gt;
&lt;strong&gt;Not great for:&lt;/strong&gt; AI agent use cases, custom React components, or teams that want open-source infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explo
&lt;/h3&gt;

&lt;p&gt;SDK-first embedded analytics. React and JavaScript components for building dashboards in your product. Built-in multi-tenancy. Good API for programmatic control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Product teams that want prebuilt dashboard components with customization.&lt;br&gt;
&lt;strong&gt;Not great for:&lt;/strong&gt; Teams that need governed metrics across multiple surfaces beyond dashboards. No semantic layer, no AI agent support.&lt;/p&gt;

&lt;h3&gt;
  
  
  Luzmo
&lt;/h3&gt;

&lt;p&gt;Embedded analytics with a focus on developer experience. SDK components, API access, and a dashboard builder. Some AI capabilities for insight generation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Small to mid-size SaaS products that want quick embedded dashboards.&lt;br&gt;
&lt;strong&gt;Not great for:&lt;/strong&gt; Enterprise multi-tenancy, AI agent integration, or teams that need metrics governance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reveal
&lt;/h3&gt;

&lt;p&gt;Embedded analytics SDK targeting .NET and Java applications. Rich visualization library. Built-in multi-tenancy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; .NET and Java shops building enterprise applications.&lt;br&gt;
&lt;strong&gt;Not great for:&lt;/strong&gt; React/TypeScript teams, AI agent use cases, or modern data stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  GoodData
&lt;/h3&gt;

&lt;p&gt;Enterprise embedded analytics platform. Strong multi-tenancy, API access, and metric definitions ("metrics as code" via MAQL). Partial open-source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Large SaaS companies with enterprise analytics requirements.&lt;br&gt;
&lt;strong&gt;Not great for:&lt;/strong&gt; Smaller teams, open-source-first teams, or AI agent integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Looker
&lt;/h3&gt;

&lt;p&gt;Google Cloud's BI platform. LookML provides a strong semantic layer, but it's locked to Looker's ecosystem. Enterprise pricing. Embedding requires Looker Embed API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Google Cloud teams with enterprise budgets that need a mature semantic layer.&lt;br&gt;
&lt;strong&gt;Not great for:&lt;/strong&gt; Teams outside the Google ecosystem, budget-conscious products, or AI agent use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Power BI Embedded
&lt;/h3&gt;

&lt;p&gt;Microsoft's embedded BI. Capacity-based pricing (SKUs). Deep Azure integration. Complex multi-tenancy setup requiring Azure AD.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Microsoft shops with Azure infrastructure.&lt;br&gt;
&lt;strong&gt;Not great for:&lt;/strong&gt; Non-Microsoft teams, simple multi-tenancy, or AI agent access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tableau Embedded
&lt;/h3&gt;

&lt;p&gt;Salesforce's visualization platform. Rich charting. Embedding requires Tableau Server/Cloud plus custom auth. Per-seat pricing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Organizations deeply invested in Tableau with budget for per-seat licensing.&lt;br&gt;
&lt;strong&gt;Not great for:&lt;/strong&gt; B2B products with many end users (per-seat cost is prohibitive). No AI agent support.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonnard
&lt;/h3&gt;

&lt;p&gt;Not an embedded BI tool. &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; is a &lt;code&gt;visualize&lt;/code&gt; tool you add to your MCP server. You call &lt;code&gt;addCharts(server, { runSql })&lt;/code&gt;, and it registers a &lt;code&gt;visualize&lt;/code&gt; tool that renders interactive charts from your query results, inside the agent. When the agent asks for a chart, your &lt;code&gt;runSql&lt;/code&gt; returns rows, Bonnard infers the chart from the typed result, and renders an interactive &lt;code&gt;ui://&lt;/code&gt; widget in Claude or ChatGPT. Chart types: line, bar, area, pie, scatter, funnel, waterfall, and table (with stacked, grouped, horizontal, and 100% bar variants). Native adapters for Postgres, BigQuery, Snowflake, Databricks, and DuckDB, or your own &lt;code&gt;runSql&lt;/code&gt;. Bonnard never touches your database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Teams already building an MCP server who want interactive charts inside Claude or ChatGPT, rendered from their own query results.&lt;br&gt;
&lt;strong&gt;Not great for:&lt;/strong&gt; Teams that want in-product dashboards for non-technical users. This is charts for AI agents, not an embedded dashboard tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision framework
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Choose dashboard embedding (Metabase, Holistics, Explo, Luzmo, Reveal)&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dashboards are the only analytics surface you need&lt;/li&gt;
&lt;li&gt;You want a prebuilt UI your customers interact with&lt;/li&gt;
&lt;li&gt;AI agent access is not a requirement&lt;/li&gt;
&lt;li&gt;You don't need the same metrics in APIs and SDKs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose enterprise BI embedding (Looker, Power BI, Tableau)&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're already in that vendor's ecosystem&lt;/li&gt;
&lt;li&gt;Enterprise pricing fits your model&lt;/li&gt;
&lt;li&gt;Internal + embedded analytics in one tool matters&lt;/li&gt;
&lt;li&gt;You have dedicated BI administrators&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose charts for AI agents via MCP (Bonnard)&lt;/strong&gt; if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're building an MCP server and want interactive charts inside Claude or ChatGPT&lt;/li&gt;
&lt;li&gt;You want charts rendered from your own query results, not a separate dashboard product&lt;/li&gt;
&lt;li&gt;Your &lt;code&gt;runSql&lt;/code&gt; returns rows and you want Bonnard to infer the chart from the typed result&lt;/li&gt;
&lt;li&gt;You want open source with a one-line install, not a dashboard license&lt;/li&gt;
&lt;li&gt;This is adjacent to embedded BI, not a replacement for it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the best free embedded analytics tool?
&lt;/h3&gt;

&lt;p&gt;Metabase (AGPL) is the most popular free option for dashboards. Superset (Apache 2.0) is free for SQL-native dashboards. If you want charts inside an AI agent rather than an embedded dashboard, &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; is a free, open-source &lt;code&gt;visualize&lt;/code&gt; tool for your MCP server. "Free" depends on what you need: basic charting, full embedded BI, or interactive charts inside Claude or ChatGPT.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is embedded analytics?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://bonnard.dev/glossary/embedded-analytics" rel="noopener noreferrer"&gt;Embedded analytics&lt;/a&gt; is analytics integrated directly into your product for end users. Instead of linking customers to a separate BI tool, charts and metrics appear inside your application. Ranges from basic iframe embedding to native SDK components with governed metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does embedded analytics cost for SaaS?
&lt;/h3&gt;

&lt;p&gt;Free (Metabase OSS) to enterprise pricing (Looker, Tableau). Mid-range tools (Explo, Holistics, Luzmo) charge per-user or usage-based, typically $500-5,000/mo. If your need is charts inside an AI agent rather than an embedded dashboard, &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; is open source and installs via npm.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which embedded analytics tool has the best multi-tenancy?
&lt;/h3&gt;

&lt;p&gt;Tools with structural multi-tenancy (enforced at the query layer): Explo, Holistics. Tools with configuration-based multi-tenancy: Metabase Enterprise (sandboxing), Power BI (Azure AD + RLS). Tools with no built-in multi-tenancy: Metabase OSS, Superset, Redash.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do any embedded analytics tools support AI agents?
&lt;/h3&gt;

&lt;p&gt;The traditional embedded analytics tools render charts in your product, not inside an AI agent. Some enterprise tools (Power BI Copilot, Luzmo, GoodData) have limited AI features but none render interactive charts inside an MCP host. If you want charts inside the agent, that's a different category: &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; adds a &lt;code&gt;visualize&lt;/code&gt; tool to your MCP server that renders interactive charts from your query results in Claude or ChatGPT. Install it with &lt;code&gt;npm install @bonnard/mcp-charts&lt;/code&gt;. Repo: &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;github.com/bonnard-data/mcp-charts&lt;/a&gt;. More in the &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP charts guide&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>saas</category>
      <category>data</category>
      <category>datavisualization</category>
    </item>
    <item>
      <title>Analytics API: How to Serve Governed Metrics to Any Consumer</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:25:33 +0000</pubDate>
      <link>https://dev.to/maxbonnard/analytics-api-how-to-serve-governed-metrics-to-any-consumer-1ko4</link>
      <guid>https://dev.to/maxbonnard/analytics-api-how-to-serve-governed-metrics-to-any-consumer-1ko4</guid>
      <description>&lt;p&gt;You have metrics. Revenue, active users, churn, usage per customer. They live in your data warehouse. Now you need to serve them to multiple consumers: your product's frontend, your customers' integrations, an AI agent, a scheduled report generator.&lt;/p&gt;

&lt;p&gt;The first instinct is custom API endpoints. One endpoint for revenue by region. Another for churn by plan. Another for the customer dashboard. Each endpoint has its own SQL query, its own response format, its own maintenance burden. By the twentieth endpoint, you're running a bespoke analytics service.&lt;/p&gt;

&lt;p&gt;An analytics API backed by a &lt;a href="https://bonnard.dev/glossary/semantic-layer" rel="noopener noreferrer"&gt;semantic layer&lt;/a&gt; gives you one query interface that serves every consumer. Define metrics once. Query them from anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an analytics API?
&lt;/h2&gt;

&lt;p&gt;An analytics API is a programmatic interface for querying metrics. Instead of connecting to a database and writing SQL, consumers call an API endpoint with the metrics they want and get structured results back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://analytics.example.com/v1/query &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer bon_pk_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "measures": ["orders.total_revenue", "orders.order_count"],
    "dimensions": ["orders.region"],
    "timeDimensions": [{
      "dimension": "orders.created_at",
      "granularity": "month",
      "dateRange": ["2026-01-01", "2026-03-31"]
    }],
    "orderBy": { "orders.total_revenue": "desc" }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response is structured JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orders.region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EMEA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orders.total_revenue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;142000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orders.order_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;340&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orders.created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-01"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orders.region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"APAC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orders.total_revenue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;98000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orders.order_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orders.created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-01-01"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every consumer uses the same interface. The dashboard frontend, the customer's API integration, the scheduled report, and the AI agent all query the same endpoint with the same metric definitions. The analytics API handles query generation, execution, caching, and access control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just build API endpoints?
&lt;/h2&gt;

&lt;p&gt;Custom endpoints work at small scale. They break at medium scale:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metric drift.&lt;/strong&gt; &lt;code&gt;/api/revenue&lt;/code&gt; and &lt;code&gt;/api/dashboard/revenue&lt;/code&gt; both return "revenue" but use different SQL queries. One gets updated. The other doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;N+1 endpoint problem.&lt;/strong&gt; Every new metric or dimension combination is a new endpoint. Revenue by region. Revenue by plan. Revenue by region AND plan. Revenue by region AND plan AND month. The combinatorial explosion is real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No caching strategy.&lt;/strong&gt; Each endpoint hits the warehouse on every request. Response times grow with data volume. You end up building a caching layer per endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No access control.&lt;/strong&gt; Each endpoint implements its own auth. Customer A's key works on all endpoints or none. Per-tenant, per-metric access control is custom code.&lt;/p&gt;

&lt;p&gt;A shared query interface handles all of this. But there's a gap that shows up once AI agents become consumers: an agent that fetches rows still has to present them. A wall of JSON tells the user nothing. Agents need to chart query results, not just fetch them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Charting the results an agent fetches
&lt;/h2&gt;

&lt;p&gt;Fetching rows is half the job. The other half is rendering them so a person can read the answer. That's where &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; fits: a &lt;code&gt;visualize&lt;/code&gt; tool you add to your MCP server that renders interactive charts from your query results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add it to your MCP server
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @bonnard/mcp-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// runSql runs against your warehouse and returns typed rows&lt;/span&gt;
&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runSql&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;addCharts&lt;/code&gt; registers a &lt;code&gt;visualize&lt;/code&gt; tool (and a &lt;code&gt;visualize_read_me&lt;/code&gt; companion) on your server. Bonnard never touches your database. Your &lt;code&gt;runSql&lt;/code&gt; does, against Postgres, BigQuery, Snowflake, Databricks, DuckDB, or any source you wire up.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the flow works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;The agent calls &lt;code&gt;visualize&lt;/code&gt; with a query.&lt;/li&gt;
&lt;li&gt;Your &lt;code&gt;runSql&lt;/code&gt; returns rows.&lt;/li&gt;
&lt;li&gt;Bonnard infers the chart from the typed result: line, bar, area, pie, scatter, funnel, waterfall, or table.&lt;/li&gt;
&lt;li&gt;The host renders an interactive &lt;code&gt;ui://&lt;/code&gt; widget inside Claude or ChatGPT.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent gets line, bar, and area charts for trends, scatter for distributions, funnel and waterfall for staged data, pie for shares, and a table when a table is the right answer. Axes, formatting, and gap-fill are decided from the typed schema, so the same query produces the same chart every time.&lt;/p&gt;

&lt;p&gt;One query interface for the numbers. One &lt;code&gt;visualize&lt;/code&gt; tool for the picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analytics API vs alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Metric governance&lt;/th&gt;
&lt;th&gt;Multi-tenant&lt;/th&gt;
&lt;th&gt;Caching&lt;/th&gt;
&lt;th&gt;Maintenance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom REST endpoints&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None (per-endpoint SQL)&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;High (per endpoint)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GraphQL API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Schema-level&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;Per-resolver&lt;/td&gt;
&lt;td&gt;Medium-high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Direct warehouse access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Low (but dangerous)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;BI tool API&lt;/strong&gt; (Looker, Metabase)&lt;/td&gt;
&lt;td&gt;Tool-specific&lt;/td&gt;
&lt;td&gt;Tool-specific&lt;/td&gt;
&lt;td&gt;Tool-specific&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bonnard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inherits your &lt;code&gt;runSql&lt;/code&gt; definitions&lt;/td&gt;
&lt;td&gt;Inherits your &lt;code&gt;runSql&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Inherits your warehouse&lt;/td&gt;
&lt;td&gt;Low (a few lines on your server). Adds interactive charts via the MCP &lt;code&gt;visualize&lt;/code&gt; tool, rendered in Claude/ChatGPT from query results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When you need an analytics API
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You need one when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple consumers query the same metrics (frontend + backend + AI agents)&lt;/li&gt;
&lt;li&gt;Customers integrate with your data via API&lt;/li&gt;
&lt;li&gt;You're building &lt;a href="https://bonnard.dev/glossary/embedded-analytics" rel="noopener noreferrer"&gt;embedded analytics&lt;/a&gt; with a frontend SDK&lt;/li&gt;
&lt;li&gt;AI agents need programmatic access to governed data&lt;/li&gt;
&lt;li&gt;Your data team is tired of building custom endpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;You don't need one when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A single dashboard covers all use cases&lt;/li&gt;
&lt;li&gt;Nobody queries your metrics programmatically&lt;/li&gt;
&lt;li&gt;The data model is so simple that direct SQL is fine&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;Once your analytics API can fetch rows, add a &lt;code&gt;visualize&lt;/code&gt; tool so agents can chart them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @bonnard/mcp-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runSql&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That registers the &lt;code&gt;visualize&lt;/code&gt; tool on your MCP server. Agents call it with a query, your &lt;code&gt;runSql&lt;/code&gt; returns rows, and Bonnard renders an interactive chart inside Claude or ChatGPT. Adapters ship for Postgres, BigQuery, Snowflake, Databricks, and DuckDB, or pass your own &lt;code&gt;runSql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the full walkthrough: &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;Add Interactive Charts to Your MCP Server&lt;/a&gt;. For the design choices behind the tool: &lt;a href="https://bonnard.dev/blog/how-bonnard-builds-agent-friendly-mcps" rel="noopener noreferrer"&gt;How Bonnard Builds Agent-Friendly MCPs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Source is on &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an analytics API?
&lt;/h3&gt;

&lt;p&gt;An analytics API is a programmatic interface for querying business metrics. Instead of writing SQL against a database, consumers call an API with the measures, dimensions, and filters they want. The API handles query generation, caching, and access control.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is an analytics API different from a REST API?
&lt;/h3&gt;

&lt;p&gt;A generic REST API exposes resources (users, orders, invoices). An analytics API exposes metrics (revenue, churn rate, active users) with aggregation, filtering, and time dimensions built in. The query interface is designed for analytical questions, not CRUD operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need an analytics API for AI agents?
&lt;/h3&gt;

&lt;p&gt;If AI agents query your data, an analytics API is the governed path. The alternative is giving agents direct database access (dangerous) or text-to-SQL (inconsistent). An analytics API backed by a semantic layer gives agents governed access to metric definitions. See &lt;a href="https://bonnard.dev/blog/what-is-agentic-semantic-layer" rel="noopener noreferrer"&gt;What Is an Agentic Semantic Layer?&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What about GraphQL for analytics?
&lt;/h3&gt;

&lt;p&gt;GraphQL works for analytics APIs but you end up building the aggregation, caching, and access control layer yourself. A semantic layer provides these out of the box with a purpose-built query interface for analytical workloads.&lt;/p&gt;

</description>
      <category>analytics</category>
      <category>api</category>
      <category>data</category>
      <category>semanticlayer</category>
    </item>
    <item>
      <title>AI Reporting: How to Automate Reports Without Losing Trust</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:25:31 +0000</pubDate>
      <link>https://dev.to/maxbonnard/ai-reporting-how-to-automate-reports-without-losing-trust-1ehb</link>
      <guid>https://dev.to/maxbonnard/ai-reporting-how-to-automate-reports-without-losing-trust-1ehb</guid>
      <description>&lt;p&gt;Your data team spends 40% of their time building reports. Weekly revenue summaries. Monthly board decks. Quarterly business reviews. Customer-facing usage reports. Each one requires pulling data, checking the numbers match last month's methodology, formatting it, and sending it out.&lt;/p&gt;

&lt;p&gt;AI reporting tools promise to automate this. Ask for a report in natural language and get charts, tables, and summaries in seconds. The problem: the AI generates the queries from scratch each time. This month's revenue calculation might differ from last month's. The board deck numbers might not match the customer report. Automation without governance creates reports nobody trusts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is AI reporting?
&lt;/h2&gt;

&lt;p&gt;AI reporting uses large language models to automate parts of the reporting workflow: querying data, generating visualizations, writing narrative summaries, and scheduling delivery. Instead of an analyst manually pulling numbers and building slides, the AI handles the mechanical work.&lt;/p&gt;

&lt;p&gt;The tools range from simple (ChatGPT generating a summary from a CSV) to sophisticated (enterprise BI platforms with AI-powered report builders). What they share: the AI interprets your data on every run. There's no guarantee that "revenue" means the same thing in this week's report as it did last week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AI-generated reports break trust
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Inconsistent methodology
&lt;/h3&gt;

&lt;p&gt;The AI generates SQL from your schema context. This week it calculates revenue as &lt;code&gt;SUM(amount)&lt;/code&gt;. Next week, with slightly different prompt context, it adds a &lt;code&gt;WHERE status = 'completed'&lt;/code&gt; filter. The CEO compares the two reports and asks why revenue dropped 8%. It didn't drop. The calculation changed. Nobody noticed because the SQL is generated on the fly.&lt;/p&gt;

&lt;h3&gt;
  
  
  No audit trail
&lt;/h3&gt;

&lt;p&gt;When the CFO asks "where did this number come from?", the answer should be a versioned metric definition, not "an LLM wrote some SQL." Automated reports need the same auditability as manual ones. If you can't reproduce a number from a specific report, the report is useless for compliance, board materials, or customer-facing delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metric drift across reports
&lt;/h3&gt;

&lt;p&gt;The weekly ops report, the monthly board deck, and the quarterly customer report all show "revenue." Without a shared definition, each report calculates it independently. The numbers diverge. Stakeholders compare reports and lose confidence. The data team gets pulled into reconciliation instead of building.&lt;/p&gt;

&lt;h2&gt;
  
  
  The governed approach to AI reporting
&lt;/h2&gt;

&lt;p&gt;The fix isn't avoiding AI. It's separating what AI is good at (generating natural language summaries, choosing visualizations, scheduling delivery) from what it's bad at (defining business metrics).&lt;/p&gt;

&lt;h3&gt;
  
  
  Define metrics once
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cubes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;revenue_metrics&lt;/span&gt;
    &lt;span class="na"&gt;sql_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;analytics.monthly_revenue&lt;/span&gt;
    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_revenue&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CASE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;WHEN&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'refunded'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AND&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'trial'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;THEN&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ELSE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;END"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Finance-approved&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;revenue&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(excludes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;refunds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;trials)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mrr&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monthly_amount&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Monthly&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;recurring&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;revenue&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;from&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;subscriptions"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;net_revenue_retention&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current_mrr&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;previous_mrr"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;avg&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NRR:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;current&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;period&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MRR&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;prior&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;period&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;MRR&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;same&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cohort"&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan_name&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;region&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;period&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;period_date&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every report, whether generated by AI or built manually, references these definitions. The methodology is fixed. This month's "revenue" uses the same calculation as last month's.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let AI handle presentation, not calculation
&lt;/h3&gt;

&lt;p&gt;The AI's job shifts from "calculate revenue" to "present the governed revenue number." It queries the &lt;a href="https://bonnard.dev/glossary/semantic-layer" rel="noopener noreferrer"&gt;semantic layer&lt;/a&gt;, gets a trustworthy number, and wraps it in a narrative: "Revenue grew 12% QoQ, driven primarily by Enterprise plan expansion in EMEA."&lt;/p&gt;

&lt;p&gt;This is a better division of labor. The data team defines what's true. The AI makes it readable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Charts in the agent, from your query results
&lt;/h3&gt;

&lt;p&gt;When the report lives inside an AI agent, the customer often wants a chart in the conversation, not only a narrative. That's a separate surface from the governance layer, and it's where &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; fits.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; is not a reporting tool or a BI platform. It is a &lt;code&gt;visualize&lt;/code&gt; tool you add to your MCP server that renders interactive charts from your query results, inside the agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runSql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;addCharts&lt;/code&gt; registers a &lt;code&gt;visualize&lt;/code&gt; tool (and &lt;code&gt;visualize_read_me&lt;/code&gt;). The agent calls &lt;code&gt;visualize&lt;/code&gt;, your &lt;code&gt;runSql&lt;/code&gt; returns rows, Bonnard infers the chart from the typed result, and it renders an interactive &lt;code&gt;ui://&lt;/code&gt; widget in Claude or ChatGPT. Chart types: line, bar, area, pie, scatter, funnel, waterfall, and table. Native adapters cover Postgres, BigQuery, Snowflake, Databricks, and DuckDB, or pass your own &lt;code&gt;runSql&lt;/code&gt;. Bonnard never touches your database.&lt;/p&gt;

&lt;p&gt;Axes, formatting, and gap-fill are determined automatically, so the same typed result produces the same chart every run. The tool returns compact summaries, row caps with completeness flags, and instructive errors with a &lt;code&gt;next_step&lt;/code&gt;, so the agent can reason about the result. For the design principles, see &lt;a href="https://bonnard.dev/blog/how-bonnard-builds-agent-friendly-mcps" rel="noopener noreferrer"&gt;how Bonnard builds agent-friendly MCPs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI report generators: what's available
&lt;/h2&gt;

&lt;p&gt;AI report generators automate the mechanical parts of building reports. Here's what exists and where each approach fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;General-purpose LLMs as report generators.&lt;/strong&gt; Upload a CSV to ChatGPT or Claude and ask for an analysis. Good for one-off reports. No governance, no scheduling, no multi-tenancy. The report is as good as the prompt. Ask the same question next month and you might get a different methodology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dedicated AI reporting tools.&lt;/strong&gt; Narrative BI, Rath, and similar tools generate automated dashboards and written insights from your data. They connect to your warehouse and produce reports on a schedule. The limitation: they define metrics at report generation time, not in a governed layer. Different reports can calculate the same metric differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BI tools with AI features.&lt;/strong&gt; Power BI Copilot, Tableau AI, Domo AI. The AI helps you build reports inside the BI tool. Better than general-purpose LLMs because the data stays in your warehouse. Limited to whatever the BI tool supports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic layer + AI.&lt;/strong&gt; Define metrics once in a governed layer. Use AI to generate narrative summaries, choose visualizations, and schedule delivery. The AI handles presentation. The semantic layer handles correctness. This is the approach that produces trustworthy automated reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI reporting tools compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Governed metrics&lt;/th&gt;
&lt;th&gt;Multi-tenant reports&lt;/th&gt;
&lt;th&gt;Audit trail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ChatGPT / Claude&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Upload data, ask for analysis&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Power BI Copilot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI in Power BI&lt;/td&gt;
&lt;td&gt;DAX measures (PBI only)&lt;/td&gt;
&lt;td&gt;Complex&lt;/td&gt;
&lt;td&gt;Within PBI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tableau AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI in Tableau&lt;/td&gt;
&lt;td&gt;Tableau model only&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Within Tableau&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domo AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AI in Domo platform&lt;/td&gt;
&lt;td&gt;Within Domo&lt;/td&gt;
&lt;td&gt;Within Domo&lt;/td&gt;
&lt;td&gt;Within Domo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Automated BI tools&lt;/strong&gt; (Narrative BI, Rath)&lt;/td&gt;
&lt;td&gt;AI-generated dashboards&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Charts in agent (Bonnard)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;visualize&lt;/code&gt; tool on your MCP server&lt;/td&gt;
&lt;td&gt;N/A (your &lt;code&gt;runSql&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;N/A (your DB)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When to use AI reporting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use AI reporting with a semantic layer when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reports go to the board, customers, or regulators (trust matters)&lt;/li&gt;
&lt;li&gt;Multiple reports use the same metrics (consistency matters)&lt;/li&gt;
&lt;li&gt;You're automating B2B customer reports with per-tenant data&lt;/li&gt;
&lt;li&gt;The data team is spending too much time on report generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use raw AI reporting tools when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One-off ad-hoc analysis for internal consumption&lt;/li&gt;
&lt;li&gt;Exploring a new dataset where methodology isn't established&lt;/li&gt;
&lt;li&gt;Speed matters more than consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;If you want charts rendered inside an AI agent from your own query results, install the package and register the tool on your MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @bonnard/mcp-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runSql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That registers the &lt;code&gt;visualize&lt;/code&gt; tool (and &lt;code&gt;visualize_read_me&lt;/code&gt;). Connect your server in Claude or ChatGPT, and the agent can render charts from your query results. Native adapters cover Postgres, BigQuery, Snowflake, Databricks, and DuckDB, or pass your own &lt;code&gt;runSql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;github.com/bonnard-data/mcp-charts&lt;/a&gt;. Full walkthrough: &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP charts&lt;/a&gt;. For background: &lt;a href="https://bonnard.dev/blog/what-is-a-semantic-layer" rel="noopener noreferrer"&gt;What Is a Semantic Layer?&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is AI reporting?
&lt;/h3&gt;

&lt;p&gt;AI reporting uses large language models to automate report generation: querying data, creating visualizations, writing summaries, and scheduling delivery. The AI handles the mechanical work that analysts currently do manually. The risk: without governed metric definitions, the AI generates different calculations on each run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can AI replace my reporting team?
&lt;/h3&gt;

&lt;p&gt;No. AI can automate the mechanical parts (pulling data, formatting charts, writing summaries). It can't define what metrics should mean, decide which numbers matter for a specific audience, or judge whether an insight is actionable. The best approach: data teams define governed metrics, AI automates the delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is automated reporting?
&lt;/h3&gt;

&lt;p&gt;Automated reporting is any system that generates reports without manual intervention. This includes scheduled dashboard refreshes, programmatic report generation via API, and AI-generated narrative summaries. The key question isn't whether to automate, but whether the automated reports use governed metric definitions.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make AI reports trustworthy?
&lt;/h3&gt;

&lt;p&gt;Separate metric definition from report generation. Define each metric once in a &lt;a href="https://bonnard.dev/glossary/semantic-layer" rel="noopener noreferrer"&gt;semantic layer&lt;/a&gt; with a fixed calculation. Let the AI query those definitions instead of generating SQL. Every report references the same versioned definitions. When someone questions a number, you point to a Git commit, not "the AI decided."&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the best approach to AI reporting for SaaS?
&lt;/h3&gt;

&lt;p&gt;Govern the numbers first so methodology stays fixed across runs. Then pick the surface where the report lands. If your customers read reports inside Claude or ChatGPT and want a chart in the conversation, &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; adds a &lt;code&gt;visualize&lt;/code&gt; tool to your MCP server that renders interactive charts from your query results. Your &lt;code&gt;runSql&lt;/code&gt; runs the query, so Bonnard never touches your database. Install with &lt;code&gt;npm install @bonnard/mcp-charts&lt;/code&gt;. Repo: &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;github.com/bonnard-data/mcp-charts&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>data</category>
      <category>aiagents</category>
      <category>analytics</category>
      <category>semanticlayer</category>
    </item>
    <item>
      <title>AI Data Analysis: Why Governed Metrics Beat Raw SQL Generation</title>
      <dc:creator>Max Mealing</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:25:28 +0000</pubDate>
      <link>https://dev.to/maxbonnard/ai-data-analysis-why-governed-metrics-beat-raw-sql-generation-19ji</link>
      <guid>https://dev.to/maxbonnard/ai-data-analysis-why-governed-metrics-beat-raw-sql-generation-19ji</guid>
      <description>&lt;p&gt;AI data analysis tools are everywhere. Upload a spreadsheet to Julius AI and get a chart. Ask ChatGPT's Data Analyst to find trends. Connect Databricks AI/BI to your warehouse and let it generate queries. The pitch: anyone can analyze data without writing SQL.&lt;/p&gt;

&lt;p&gt;The pitch works for one-off exploration. It breaks when you need trustworthy numbers at scale: consistent results across tools, governed access per customer, and an audit trail for every answer. That's where most AI analytics tools stop and where a semantic layer starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is AI data analysis?
&lt;/h2&gt;

&lt;p&gt;AI data analysis uses large language models and machine learning to automate parts of the data analysis workflow: generating queries, identifying patterns, creating visualizations, and summarizing findings in natural language. Instead of writing SQL by hand, you describe what you want and the AI produces it.&lt;/p&gt;

&lt;p&gt;The tools fall into three categories:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spreadsheet AI.&lt;/strong&gt; Upload a CSV and ask questions. Julius AI, Formula Bot, ChatGPT Data Analyst. Good for ad-hoc exploration of small datasets. No governance, no multi-tenancy, no audit trail. Your data leaves your infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BI tool AI.&lt;/strong&gt; Built into an existing BI platform. Tableau's AI analytics, Power BI Copilot, Domo AI, ThoughtSpot's natural language search. Better than spreadsheet tools because they query your warehouse. Still limited by the BI tool's ecosystem and licensing model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text-to-SQL agents.&lt;/strong&gt; An LLM generates SQL against your warehouse schema. DataGPT, custom GPT agents, Databricks AI/BI. Most flexible, most dangerous. The agent writes arbitrary SQL. No guardrails on what it queries or how it calculates metrics.&lt;/p&gt;

&lt;p&gt;All three share the same fundamental problem: the AI interprets your data on every query. There's no shared definition of what "revenue" means. Different tools, different phrasings, different numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The accuracy problem nobody talks about
&lt;/h2&gt;

&lt;p&gt;AI data analysis demos are impressive. The tool generates a chart from a natural language question in seconds. But run the same question through two different tools and compare the numbers. They don't match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; Because each tool independently interprets your schema. "Revenue" might be &lt;code&gt;SUM(amount)&lt;/code&gt; in one tool and &lt;code&gt;SUM(total_price) WHERE status = 'completed'&lt;/code&gt; in another. Both are plausible. Neither knows that your finance team excludes refunds and trial conversions.&lt;/p&gt;

&lt;p&gt;This isn't a model quality issue. GPT-4, Claude, and Gemini all exhibit this behavior. The model does what it's designed to do: generate plausible SQL from schema context. But "plausible" and "correct" are different things.&lt;/p&gt;

&lt;p&gt;The accuracy problem compounds at scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One analyst, one question, one tool.&lt;/strong&gt; Accuracy is "good enough." You eyeball the result and know if it's in the right ballpark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple analysts, same question, different tools.&lt;/strong&gt; The numbers diverge. Debates about "which dashboard is right" start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customers querying their own data.&lt;/strong&gt; You can't tell customers "the number is approximately right." They expect precision. One wrong number and trust is gone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI agents querying autonomously.&lt;/strong&gt; No human in the loop to sanity-check. Wrong numbers propagate through workflows without anyone noticing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://bonnard.dev/glossary/text-to-sql" rel="noopener noreferrer"&gt;Text-to-SQL&lt;/a&gt; gives you speed. It doesn't give you trust. For production analytics, you need both.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes AI analytics production-grade?
&lt;/h2&gt;

&lt;p&gt;The gap between a demo and a production system is five things. Most AI data analysis tools have one or two. None of the spreadsheet or text-to-SQL tools have all five.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Governed metric definitions
&lt;/h3&gt;

&lt;p&gt;Every metric has one definition. "Revenue" is &lt;code&gt;SUM(amount) WHERE status != 'refunded' AND type != 'trial'&lt;/code&gt;. This definition is fixed, versioned in Git, and referenced by every consumer. The AI doesn't interpret what revenue means. It queries the governed definition.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cubes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;orders&lt;/span&gt;
    &lt;span class="na"&gt;sql_table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;public.orders&lt;/span&gt;
    &lt;span class="na"&gt;measures&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total_revenue&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CASE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;WHEN&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'refunded'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AND&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;!=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'trial'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;THEN&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ELSE&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;END"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sum&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;revenue&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;excluding&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;refunds&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;trials"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_count&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;count&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;average_order_value&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amount&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;avg&lt;/span&gt;
    &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;category&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;category&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;created_at&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a &lt;a href="https://bonnard.dev/glossary/semantic-layer" rel="noopener noreferrer"&gt;semantic layer&lt;/a&gt;: a metadata layer between your warehouse and every consumer. The AI agent calls &lt;code&gt;explore_schema&lt;/code&gt; to discover available metrics, then &lt;code&gt;query&lt;/code&gt; to fetch governed data. It never generates SQL from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Consistency across every surface
&lt;/h3&gt;

&lt;p&gt;The same metric definition serves dashboards, React components in your product, REST APIs, AI agents, and markdown reports. Ask for "revenue" from any surface and you get the same number. Not because the AI happened to generate the same SQL, but because the definition is fixed.&lt;/p&gt;

&lt;p&gt;This matters for AI specifically because AI tools generate different SQL on every call. Even the same tool, same question, different session can produce different queries. Governed definitions eliminate this variance.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Multi-tenant access control
&lt;/h3&gt;

&lt;p&gt;For B2B products, Customer A's AI agent should never see Customer B's data. This can't be a prompt instruction ("only query data for customer A"). It needs to be structural: enforced on every query regardless of what the AI does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;security_context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tenant_filter&lt;/span&gt;
        &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{SECURITY_CONTEXT.tenant_id}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;customer_id"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every query through a tenant's key automatically includes this filter. The AI agent can't bypass it. See &lt;a href="https://bonnard.dev/glossary/rbac" rel="noopener noreferrer"&gt;RBAC&lt;/a&gt; for more on how this works.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Performance at AI scale
&lt;/h3&gt;

&lt;p&gt;AI agents make more queries than humans. A human asks one question. An agent might make 10 queries to answer it: exploring the schema, trying different dimensions, following up on anomalies. Multiply by hundreds of customers' agents querying simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://bonnard.dev/glossary/pre-aggregation" rel="noopener noreferrer"&gt;Pre-aggregation&lt;/a&gt; caching pre-computes common rollups. Hot queries return in single-digit milliseconds. Without it, AI-scale query volumes overwhelm most warehouses.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Audit trail
&lt;/h3&gt;

&lt;p&gt;When the number is wrong, you need to trace it. With a semantic layer, every result traces back to a versioned metric definition in Git. You can point to the exact commit that defined the calculation, see when it changed, and who approved the change. With raw text-to-SQL, the trail is "an LLM generated some SQL" with no reproducibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI data analysis tools compared
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Governed metrics&lt;/th&gt;
&lt;th&gt;Multi-tenant&lt;/th&gt;
&lt;th&gt;Audit trail&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Julius AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Spreadsheet upload + LLM&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Ad-hoc exploration of CSVs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ChatGPT Data Analyst&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;File upload + code interpreter&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;One-off analysis, prototyping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Databricks AI/BI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Text-to-SQL + Unity Catalog&lt;/td&gt;
&lt;td&gt;Partial (table-level)&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;Teams already on Databricks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Power BI Copilot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Natural language in Power BI&lt;/td&gt;
&lt;td&gt;DAX measures (locked to PBI)&lt;/td&gt;
&lt;td&gt;Complex setup&lt;/td&gt;
&lt;td&gt;Within PBI&lt;/td&gt;
&lt;td&gt;Microsoft ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tableau AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Natural language in Tableau&lt;/td&gt;
&lt;td&gt;Tableau semantic model&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Within Tableau&lt;/td&gt;
&lt;td&gt;Salesforce ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ThoughtSpot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Natural language BI&lt;/td&gt;
&lt;td&gt;Proprietary semantic layer&lt;/td&gt;
&lt;td&gt;Enterprise&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Enterprise AI-powered BI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DataGPT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Conversational analytics agent&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Autonomous data analysis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domo AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BI platform with AI features&lt;/td&gt;
&lt;td&gt;Within Domo&lt;/td&gt;
&lt;td&gt;Within Domo&lt;/td&gt;
&lt;td&gt;Within Domo&lt;/td&gt;
&lt;td&gt;End-to-end data platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Charts in agent (Bonnard)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;visualize&lt;/code&gt; tool on your MCP server&lt;/td&gt;
&lt;td&gt;N/A (your &lt;code&gt;runSql&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;N/A (your DB)&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;td&gt;Interactive charts inside Claude or ChatGPT&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tools in the top half are AI-first but governance-second. The ones in the middle (enterprise BI) have governance but lock it to their ecosystem. The bottom row is a different kind of option: &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; does not generate SQL or define metrics. It renders interactive charts from the rows your &lt;code&gt;runSql&lt;/code&gt; returns, inside the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  When each approach makes sense
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use spreadsheet AI (Julius, ChatGPT)&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're exploring a dataset for the first time&lt;/li&gt;
&lt;li&gt;Nobody else needs to reproduce the result&lt;/li&gt;
&lt;li&gt;The data isn't sensitive&lt;/li&gt;
&lt;li&gt;You want speed over accuracy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use BI tool AI (Power BI Copilot, Tableau AI)&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're already in that BI ecosystem&lt;/li&gt;
&lt;li&gt;Internal teams are the primary consumer&lt;/li&gt;
&lt;li&gt;You don't need to serve AI agents or embed in products&lt;/li&gt;
&lt;li&gt;Enterprise licensing fits your budget&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Use a semantic layer&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple consumers need the same metrics (dashboards + API + agents)&lt;/li&gt;
&lt;li&gt;Customers see the results (B2B &lt;a href="https://bonnard.dev/glossary/embedded-analytics" rel="noopener noreferrer"&gt;embedded analytics&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;AI agents query data autonomously&lt;/li&gt;
&lt;li&gt;You need auditability (which definition produced this number?)&lt;/li&gt;
&lt;li&gt;Metrics must be consistent across every tool and surface&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting the chart back: charts inside the agent
&lt;/h2&gt;

&lt;p&gt;Governing the numbers is one half of AI data analysis. The other half is what the customer actually sees. When someone asks a question in Claude or ChatGPT, they often want a chart, not a table of rows. That's a separate surface from the governance layer, and it's where &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; fits.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; is not a semantic layer, a BI tool, or a text-to-SQL agent. It is a &lt;code&gt;visualize&lt;/code&gt; tool you add to your MCP server that renders interactive charts from your query results, inside the agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runSql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;addCharts&lt;/code&gt; registers a &lt;code&gt;visualize&lt;/code&gt; tool (and a &lt;code&gt;visualize_read_me&lt;/code&gt; companion). The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The agent calls &lt;code&gt;visualize&lt;/code&gt; with a query.&lt;/li&gt;
&lt;li&gt;Your &lt;code&gt;runSql&lt;/code&gt; returns rows.&lt;/li&gt;
&lt;li&gt;Bonnard infers the chart from the typed result.&lt;/li&gt;
&lt;li&gt;It renders an interactive &lt;code&gt;ui://&lt;/code&gt; widget in Claude or ChatGPT.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Chart types: line, bar, area, pie, scatter, funnel, waterfall, and table. Axes, formatting, and gap-fill are determined automatically, so the same typed result produces the same chart. Native adapters cover Postgres, BigQuery, Snowflake, Databricks, and DuckDB, or pass your own &lt;code&gt;runSql&lt;/code&gt;. Bonnard never touches your database.&lt;/p&gt;

&lt;p&gt;The tool is built for the agent: compact summaries, row caps with completeness flags, a typed schema, and instructive errors with a &lt;code&gt;next_step&lt;/code&gt;. For the design principles, see &lt;a href="https://bonnard.dev/blog/how-bonnard-builds-agent-friendly-mcps" rel="noopener noreferrer"&gt;how Bonnard builds agent-friendly MCPs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shift from AI-generated SQL to AI-queried metrics
&lt;/h2&gt;

&lt;p&gt;The first generation of AI analytics gave agents raw warehouse access and hoped for the best. It worked in demos. It failed in production because the agent interprets business logic on every query.&lt;/p&gt;

&lt;p&gt;The next generation separates the concerns. Data teams define governed metrics in a &lt;a href="https://bonnard.dev/glossary/semantic-layer" rel="noopener noreferrer"&gt;semantic layer&lt;/a&gt;. AI agents query those definitions instead of raw tables. The data team's expertise is encoded in the schema. The AI's role shifts from "guess what revenue means" to "pick the right governed metric and present the result."&lt;/p&gt;

&lt;p&gt;This is a better division of labor. The data team defines what's true. The AI handles the interface. Trustworthy numbers with a natural language frontend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;If you want charts rendered inside an AI agent from your own query results, install the package and register the tool on your MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @bonnard/mcp-charts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;addCharts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@bonnard/mcp-charts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;addCharts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runSql&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That registers the &lt;code&gt;visualize&lt;/code&gt; tool (and &lt;code&gt;visualize_read_me&lt;/code&gt;). Connect your server in Claude or ChatGPT, and the agent can render charts from your query results. Native adapters cover Postgres, BigQuery, Snowflake, Databricks, and DuckDB, or pass your own &lt;code&gt;runSql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/bonnard-data/mcp-charts" rel="noopener noreferrer"&gt;github.com/bonnard-data/mcp-charts&lt;/a&gt;. Full walkthrough: &lt;a href="https://bonnard.dev/blog/mcp-charts" rel="noopener noreferrer"&gt;MCP charts&lt;/a&gt;. For background: &lt;a href="https://bonnard.dev/blog/what-is-a-semantic-layer" rel="noopener noreferrer"&gt;What Is a Semantic Layer?&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is AI data analysis?
&lt;/h3&gt;

&lt;p&gt;AI data analysis uses large language models and machine learning to automate data querying, pattern detection, and insight generation. Instead of writing SQL manually, you ask questions in natural language and the AI produces queries, charts, and summaries. Tools range from spreadsheet analyzers (Julius AI) to enterprise BI platforms with AI features (Power BI Copilot, Tableau AI).&lt;/p&gt;

&lt;h3&gt;
  
  
  Is AI data analysis accurate?
&lt;/h3&gt;

&lt;p&gt;For ad-hoc exploration, accuracy is "good enough." For production analytics where customers see the numbers, accuracy varies per query because the AI generates SQL from scratch each time. Two tools generating SQL for "revenue" may produce different numbers because they interpret the calculation differently. A semantic layer solves this by defining each metric once and letting AI query the definition instead of generating its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the best AI tool for data analysis?
&lt;/h3&gt;

&lt;p&gt;It depends on the use case. For one-off CSV exploration: Julius AI or ChatGPT Data Analyst. For enterprise BI: Power BI Copilot or Tableau AI. For governed metrics served to AI agents and B2B customers: a semantic layer approach. See the comparison table above.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the best approach to AI data analysis for SaaS?
&lt;/h3&gt;

&lt;p&gt;For a SaaS product, two things matter: trustworthy numbers and where the customer sees the result. Govern the numbers with a semantic layer so the AI queries fixed definitions instead of guessing. Then decide the surface. If your customers work in Claude or ChatGPT, &lt;code&gt;@bonnard/mcp-charts&lt;/code&gt; adds a &lt;code&gt;visualize&lt;/code&gt; tool to your MCP server that renders interactive charts from your query results, inside the agent. Your &lt;code&gt;runSql&lt;/code&gt; runs the query, so Bonnard never touches your database. Install with &lt;code&gt;npm install @bonnard/mcp-charts&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is AI analytics different from traditional BI?
&lt;/h3&gt;

&lt;p&gt;Traditional BI requires analysts to write queries and build dashboards. AI analytics automates the query generation. The risk: AI-generated queries are inconsistent. Traditional BI with a semantic layer gives you consistent definitions. AI analytics with a semantic layer gives you both: consistent definitions with a natural language interface.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can AI replace data analysts?
&lt;/h3&gt;

&lt;p&gt;No. AI can generate queries and create visualizations faster than humans. It can't define what metrics should mean, decide which questions to ask, or judge whether an insight is actionable. Data analysts bring domain knowledge and judgment. AI handles the mechanical parts. A semantic layer encodes the analyst's expertise so AI can query it reliably.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between AI analytics and text-to-SQL?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://bonnard.dev/glossary/text-to-sql" rel="noopener noreferrer"&gt;Text-to-SQL&lt;/a&gt; is a technique where an LLM generates SQL from natural language. AI analytics is broader: it includes text-to-SQL plus pattern detection, anomaly identification, forecasting, and visualization generation. Both share the same governance gap: the AI interprets your data on every query. A semantic layer addresses this for both.&lt;/p&gt;

&lt;h3&gt;
  
  
  What warehouses work with AI data analysis?
&lt;/h3&gt;

&lt;p&gt;Most AI analytics tools work with common warehouses: &lt;a href="https://bonnard.dev/integrations/snowflake" rel="noopener noreferrer"&gt;Snowflake&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/bigquery" rel="noopener noreferrer"&gt;BigQuery&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/databricks" rel="noopener noreferrer"&gt;Databricks&lt;/a&gt;, &lt;a href="https://bonnard.dev/integrations/postgres" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt; (including Supabase, Neon, and RDS), &lt;a href="https://bonnard.dev/integrations/redshift" rel="noopener noreferrer"&gt;Redshift&lt;/a&gt;, and &lt;a href="https://bonnard.dev/integrations/duckdb" rel="noopener noreferrer"&gt;DuckDB&lt;/a&gt; (including MotherDuck). The semantic layer generates the appropriate SQL dialect for each warehouse.&lt;/p&gt;

</description>
      <category>data</category>
      <category>sql</category>
      <category>aiagents</category>
      <category>semanticlayer</category>
    </item>
  </channel>
</rss>
