<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[GenBI in Practice]]></title><description><![CDATA[Data engineer. Years of pipelines and dashboards. Now exploring how GenBI tools like Wren AI and Databricks Genie are changing the way we turn data into insight]]></description><link>https://zhaidata.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>GenBI in Practice</title><link>https://zhaidata.com</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 17 Oct 2026 03:45:19 GMT</lastBuildDate><atom:link href="https://zhaidata.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The 74-Point Accuracy Gap: Why AI Analytics Fails Without Human Curation (And How to Fix It)]]></title><description><![CDATA[Anthropic recently published how they run self-service data analytics internally. One number stood out: without structured skills docs, their AI agent scored 21% accuracy. With them, it hit 95%+.
That]]></description><link>https://zhaidata.com/the-74-point-accuracy-gap-why-ai-analytics-fails-without-human-curation-and-how-to-fix-it</link><guid isPermaLink="true">https://zhaidata.com/the-74-point-accuracy-gap-why-ai-analytics-fails-without-human-curation-and-how-to-fix-it</guid><category><![CDATA[context engineering]]></category><category><![CDATA[analytics engineering]]></category><category><![CDATA[text to sql]]></category><category><![CDATA[#Semantic Layer]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Sean Liu]]></dc:creator><pubDate>Wed, 23 Sep 2026 01:59:16 GMT</pubDate><content:encoded><![CDATA[<p>Anthropic recently published how they run self-service data analytics internally. One number stood out: <strong>without structured skills docs, their AI agent scored 21% accuracy. With them, it hit 95%+.</strong></p>
<p>That 74-percentage-point gap wasn't closed by a better model, a fancier framework, or a new tool. It was closed by people writing markdown files.</p>
<p>This article breaks down what Anthropic tested, what didn't work, and a generalizable process any data team can follow to build the artifacts that actually move the needle.</p>
<hr />
<h2>What Anthropic Tested — And What Failed</h2>
<p>Before we talk about what works, it's worth understanding what Anthropic tried that <strong>didn't</strong> work. These findings challenge common assumptions about AI analytics.</p>
<h3>Failed: Auto-generated semantic layer definitions</h3>
<p>Anthropic tried having an LLM auto-generate metric definitions from raw tables and query logs. The result was <strong>net-negative</strong> on their evaluation set — worse than having no semantic layer at all.</p>
<p>Why? The auto-generated definitions produced plausible-looking metrics that actually encoded the very ambiguities the team was trying to eliminate. When "revenue" has three valid interpretations in your data warehouse, an LLM will pick one without understanding why the others exist.</p>
<p><strong>Takeaway:</strong> Use AI to generate <em>documentation</em> (descriptions, summaries). Have a human own the <em>definition</em> (what the metric means, how it's calculated, which table is canonical).</p>
<h3>Failed: Raw query corpus access</h3>
<p>This is the surprising one. Anthropic gave their agent direct <code>grep</code> access to their entire corpus of dashboard SQL, transformation code, and analyst notebooks — thousands of files. They verified in transcripts that the agent actually read the relevant queries before answering.</p>
<p>Accuracy moved by <strong>less than one percentage point</strong>.</p>
<p>They checked the obvious confounds: Was the correct answer in the corpus for questions it got wrong? About 80% of the time, yes. Did "answer present" predict "now gets it right"? No — the flip rate was flat.</p>
<p><strong>The information was there. The agent saw it. It still didn't use it correctly.</strong></p>
<p>The bottleneck wasn't <em>access</em> to prior work. It was <em>structure</em> — mapping a question to the right entity. Unstructured retrieval couldn't connect a new question to the right precedent.</p>
<h3>Failed: Additional rounds of doc refinement past a certain point</h3>
<p>They hit three consecutive net-negative iterations where docs were getting longer, not better. More words didn't mean more accuracy.</p>
<h3>Failed: Cheaper model for adversarial review</h3>
<p>Swapping the adversarial reviewer to a cheaper model to cut latency lost most of the accuracy gains without meaningful speedup.</p>
<hr />
<h2>What Actually Worked</h2>
<p>Three artifacts accounted for nearly all of the accuracy improvement:</p>
<table>
<thead>
<tr>
<th>Artifact</th>
<th>What it does</th>
<th>Impact</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Skills</strong> (structured markdown instructions)</td>
<td>Routes the agent to the right sources in the right order</td>
<td>21% → 95%+ accuracy</td>
</tr>
<tr>
<td><strong>Human-curated semantic layer</strong></td>
<td>Single source of truth for metric definitions</td>
<td>Mandatory first step for every query</td>
</tr>
<tr>
<td><strong>Curated reference docs</strong></td>
<td>Domain-specific tables, gotchas, and patterns distilled from the query corpus</td>
<td>Solved retrieval failure</td>
</tr>
</tbody></table>
<p>A fourth technique — an <strong>adversarial review sub-agent</strong> — added 6% accuracy on top, at the cost of 32% more tokens and 72% higher latency.</p>
<hr />
<h2>The Process: Building These Artifacts From What You Already Have</h2>
<p>The knowledge required to build these artifacts isn't new. It already exists in your organization — scattered across dashboards, Slack threads, analyst notebooks, and people's heads. The process is extraction and structuring, not creation from scratch.</p>
<h3>Phase 1: Mine Your Existing Resources (1–2 weeks per domain)</h3>
<h4>Step 1 — Identify your top 20 business questions per domain</h4>
<p>Your company already asks these questions repeatedly. Find them in:</p>
<ul>
<li><strong>Dashboard titles and chart labels.</strong> Every chart is essentially a natural language question with validated SQL behind it. "Monthly Revenue by Region" is both the question and proof that someone already wrote correct SQL to answer it.</li>
<li><strong>Slack channels</strong> where people request data from your team. These contain the real phrasing, real ambiguity, and real business jargon your users actually use. Search for patterns like "can someone pull..." or "what's the latest..."</li>
<li><strong>Query logs.</strong> Every warehouse (Snowflake, BigQuery, Databricks) records query history. Filter to analytical queries (not ETL jobs), cluster by business domain, and identify the most frequently run patterns.</li>
<li><strong>Recurring meeting agendas.</strong> "Review weekly active users" and "check pipeline by region" are business questions hiding in plain sight.</li>
</ul>
<p>Don't try to cover everything. Anthropic found diminishing returns past a few dozen evals per topic. Start with the questions that get asked every week.</p>
<h4>Step 2 — For each question, trace back to the canonical answer</h4>
<p>Open the dashboard or notebook that answers it. Find the SQL. For each question-SQL pair, document:</p>
<ul>
<li>Which tables and columns were used</li>
<li>Which filters were applied (and why)</li>
<li>Which definition of the metric was used</li>
<li>What a senior analyst would warn you about</li>
</ul>
<p><strong>This is where you discover ambiguity.</strong> You'll find that "revenue" is calculated differently in three dashboards. That's not a bug — it's the most valuable finding. Resolve it now with your business stakeholders and pick one canonical definition.</p>
<h4>Step 3 — Document the gotchas</h4>
<p>Ask your most senior analyst: <em>"What do new hires get wrong in their first month?"</em> That list is your gotchas doc:</p>
<ul>
<li>"This field double-counts if you don't filter by X"</li>
<li>"Status = 4 means refunded, not cancelled"</li>
<li>"This table has a 2-day lag — always use MAX(date), not 'yesterday'"</li>
<li>"We changed product names in Q3 2025 — old values are frozen in the data"</li>
</ul>
<p>These are the things that make an answer go from "valid SQL" to "correct business answer."</p>
<hr />
<h3>Phase 2: Structure the Artifacts</h3>
<p>Now convert what you found into the three artifacts that Anthropic showed actually work.</p>
<h4>Artifact 1: Reference Docs (One Per Domain)</h4>
<p>This is the most impactful artifact. It replaces unstructured tribal knowledge with a document an AI agent can reliably navigate.</p>
<p>Anthropic shared the skeleton they use internally. Here it is, adapted with annotations:</p>
<pre><code class="language-markdown"># [Domain] Tables

## Quick Reference
### Business Context
[What this domain means in plain words. Not technical —
write it for someone who just joined the company.]

### Entity Grain
[What one row represents. This single line prevents
the most common join errors.]

### Standard Hygiene Filter
[The filter every query in this domain applies.
Example: "WHERE is_test_account = FALSE AND event_date &gt;= '2024-01-01'"]

## Dimensions
[How the key dimensions are encoded, and how the same
concept is named differently across tables.
Example: "region" in table A = "geo" in table B = "market" in table C]

## Key Tables

### [table_name]
- **Grain**: [what one row represents]
- **Scope/exclusions**: [what's included, what's deliberately left out]
- **Usage**: [when to use it, when NOT to use it, join keys, required filters]

### [table_name_2]
[... one section per governed table ...]

## Gotchas
[The wrong-answer modes a senior analyst would warn you about.
These are the things that make the difference between
"valid SQL" and "correct business answer."]

## Best Practices / Common Query Patterns
[Default choices, standard cuts, worked patterns where
the exact query form is the hard part.]

## Cross-References
[Neighboring domain docs that own adjacent questions.
"For customer lifetime value, see finance/ltv.md"]
</code></pre>
<p><strong>Key principle:</strong> Write these docs for retrieval by an AI agent, not for human reading. That means explicit routing triggers ("IF the question is about experiment lift... DO NOT use for raw event counts"), not narrative explanations.</p>
<h4>Artifact 2: Skills (Routing Instructions)</h4>
<p>Skills are what turned Anthropic's accuracy from 21% to 95%. They're the procedural knowledge: which sources to consult in what order.</p>
<p>Anthropic's approach uses two paired skills per domain:</p>
<p><strong>Knowledge skill</strong> — a thin router that narrows the search space:</p>
<pre><code class="language-markdown">---
name: [domain]-knowledge
description: "IF the user asks about [domain topics] — THEN invoke this skill."
---

# [Domain] Knowledge

## Semantic Layer (REQUIRED first step)
The governed semantic layer is the mandatory default path for
every data question. Raw SQL is the fallback, used only after
the semantic layer is shown not to cover the ask.

## Required workflow
1. **Search** the semantic layer for matching metrics/dimensions
2. **Check segments** — named canonical population filters
   (hand-rolled WHERE clauses for these are the dominant wrong-answer mode)
3. **Compile and run** if found
4. **Fallback** to reference docs ONLY if no semantic layer coverage

&gt; Don't bail early. Do NOT fall back to raw SQL because:
&gt; - "Needs a custom date filter" → [covered by time-dimension specs]
&gt; - "Needs a join" → [the metric layer already encapsulates joins]

## Date Conventions
- "Last week/month" → last *complete* calendar week/month, not trailing-7/30
- Timezone default: [your timezone]
- Freshness lag: [which tables settle late]

## Reference Docs Navigation
### [Sub-domain A] → `references/[sub_domain_a].md`
- **Use for**: [kinds of questions]
- **Key tables**: [...]

### [Sub-domain B] → `references/[sub_domain_b].md`
- **Use for**: [...]

## Gotchas
- Use `[field_x_v2]` NOT `[field_x]`
- [Two tables report the same metric at different grains — which to use]
- [Which of two plausible sources is canonical]
</code></pre>
<p><strong>Runbook skill</strong> — encodes the analysis process:</p>
<pre><code class="language-markdown">---
name: [domain]-runbook
description: "Execution process for [domain] analysis questions."
---

# [Domain] Analysis Runbook

## Step 1: Clarify the question
- Time period? Segment? What business decision does this inform?
- Disambiguate terms (see Entity Disambiguation in knowledge skill)

## Step 2: Check for existing dashboards
- [Link to domain dashboard catalog]

## Step 3: Find sources
- Invoke [domain]-knowledge skill
- Follow its semantic layer → reference doc → raw table priority

## Step 4: Execute and validate
- Run the query
- Apply adversarial review: challenge all assumptions
- Check data freshness

## Step 5: Report with provenance
Every answer ends with:
&gt; **Source:** [semantic layer | governed table | raw exploration]
&gt; **Freshness:** [max date in data]
&gt; **Owner:** [owning team]

## Reusable Analysis Patterns
### Retention curve
[Standard approach for this domain]

### Funnel analysis
[Standard approach]

### Rate decomposition
[Standard approach]
</code></pre>
<h4>Artifact 3: Semantic Layer (Human-Curated Metrics)</h4>
<p>Start small: just the top 10–15 metrics per domain. For each one, you need:</p>
<ul>
<li>The exact calculation</li>
<li>The grain</li>
<li>Required filters (always applied)</li>
<li>Dimensions it can be sliced by</li>
<li>Who owns it</li>
</ul>
<p>The tool doesn't matter — dbt MetricFlow, Cube, warehouse-native semantic views, or even a well-structured YAML file. What matters is that a human defined it and a business stakeholder signed off.</p>
<p><strong>Do not auto-generate these with an LLM.</strong> Anthropic tested it. It was net-negative.</p>
<hr />
<h3>Phase 3: Validate and Maintain</h3>
<h4>Build your evaluation set</h4>
<p>Your top 20 questions per domain from Phase 1, paired with verified answers, become your offline eval set. Format:</p>
<pre><code class="language-json">{
  "domain": "revenue",
  "question": "What was total revenue by region last month?",
  "gold_sql": "SELECT region, SUM(amount) FROM fct_orders WHERE ...",
  "expected_behavior": "Should use semantic layer metric 'total_revenue'",
  "snapshot_date": "2026-09-01",
  "difficulty": "simple",
  "owner": "finance-analytics"
}
</code></pre>
<p>Anthropic's guidance:</p>
<ul>
<li><strong>Target ~100% offline accuracy</strong> before launching a domain</li>
<li><strong>Pin every eval to a snapshot date</strong> — an eval against live data goes stale when the underlying number moves</li>
<li><strong>Store results like telemetry</strong>, with skill version, model ID, and per-assertion pass/fail</li>
<li><strong>Gate launches per domain</strong> — a domain owner can't announce the agent to stakeholders until their eval set clears threshold</li>
</ul>
<h4>Maintain like a first-class product</h4>
<p>Anthropic watched accuracy drift from ~95% to ~65% within a month when they stopped maintaining docs. Their solution:</p>
<ul>
<li><strong>Colocate everything in one repo.</strong> Skill markdown, reference docs, and transformation models live together. The PR that changes a data model must also update the docs.</li>
<li><strong>CI hook</strong> flags any reporting-model change that doesn't touch a skill file</li>
<li><strong>90% of data-model PRs</strong> now include a skill change in the same diff</li>
<li><strong>Harvest corrections.</strong> A scheduled agent scans Slack for correction language, drafts a fix to the relevant reference doc, and opens a PR</li>
</ul>
<hr />
<h2>The Uncomfortable Truth</h2>
<p>The most valuable skill in AI-powered analytics isn't prompt engineering, model selection, or framework architecture. It's the unglamorous work of writing and maintaining structured documentation about what your data actually means.</p>
<p>This can't be automated. The knowledge is tribal — it lives in people's heads, in Slack threads, in the footnotes of dashboards nobody reads. The job is extracting that knowledge into a structured format an AI agent can follow.</p>
<p>Anthropic proved the math: 21% without human curation, 95%+ with it. The gap isn't closed by better models. It's closed by analytics engineers doing what they've always done — understanding the business — and writing it down in a way machines can use.</p>
<hr />
<h2>Sources</h2>
<ul>
<li><a href="https://claude.com/blog/how-anthropic-enables-self-service-data-analytics-with-claude">How Anthropic enables self-service data analytics with Claude</a> — Anthropic, June 2026</li>
<li><a href="https://arxiv.org/html/2409.02038v3">BEAVER: An Enterprise Benchmark for Text-to-SQL</a> — The first text-to-SQL benchmark derived from real enterprise data warehouses</li>
<li><a href="https://arxiv.org/html/2507.14372v1">Text-to-SQL for Enterprise Data Analytics</a> — LinkedIn's internal SQL Bot paper, reporting 53% accuracy on enterprise data</li>
<li><a href="https://www.getdbt.com/blog/from-analytics-engineer-to-context-engineer">From analytics engineer to context engineer</a> — dbt Labs, on the role shift</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[My semantic layer answers this question confidently. The answer is ambiguous.]]></title><description><![CDATA[I'm building an evaluation harness for AI-generated SQL. While simplifying my semantic layer, I accidentally removed a guardrail I didn't know I had — and discovered a class of question that conventio]]></description><link>https://zhaidata.com/my-semantic-layer-answers-this-question-confidently-the-answer-is-ambiguous</link><guid isPermaLink="true">https://zhaidata.com/my-semantic-layer-answers-this-question-confidently-the-answer-is-ambiguous</guid><category><![CDATA[AI]]></category><category><![CDATA[SQL]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[genbi]]></category><category><![CDATA[dbt]]></category><category><![CDATA[analytics]]></category><dc:creator><![CDATA[Sean Liu]]></dc:creator><pubDate>Thu, 10 Sep 2026 03:50:18 GMT</pubDate><content:encoded><![CDATA[<p>I'm building an evaluation harness for AI-generated SQL. While simplifying my semantic layer, I accidentally removed a guardrail I didn't know I had — and discovered a class of question that conventional semantic layers don't handle well.</p>
<h3>The question</h3>
<p>"What was average order value by product category in 2023?"</p>
<p>This sounds straightforward. It isn't. The problem is that 28.62% of orders in this dataset span multiple product categories. An order with a jacket ($100) and socks ($15) belongs to both Outerwear and Accessories.</p>
<p>So "AOV by category" requires a decision about how to attribute multi-category orders, and there are at least four defensible methods:</p>
<p><strong>Attribute the full order to every category it touches.</strong> The Outerwear row shows $115 (the whole order), and so does the Accessories row. Revenue double-counts across categories. AOV is high everywhere.</p>
<p><strong>Attribute only the lines belonging to the category.</strong> Outerwear shows $100, Accessories shows $15. Revenue doesn't double-count, but the denominator is "orders touching this category," which still includes the multi-category order in both rows. You're dividing line-level revenue by order-level counts — a mixed-grain ratio.</p>
<p><strong>Split the order proportionally.</strong> Outerwear gets 100/115 of the order, Accessories gets 15/115. Clean in theory, almost never how anyone actually computes it.</p>
<p><strong>Exclude multi-category orders.</strong> Only single-category orders contribute. Clean, but you've just dropped 28.62% of your data.</p>
<p>Four methods, four numbers, all defensible. The correct behavior for any analytics system is to ask which one the user means.</p>
<h3>What my semantic layer does instead</h3>
<p>I have a <a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce">MetricFlow semantic layer</a> defined over <code>fct_order_items</code> at line-item grain. The metric is:</p>
<pre><code class="language-yaml">- name: average_order_value
  type: ratio
  type_params:
    numerator: net_revenue
    denominator: order_count
</code></pre>
<p>Where <code>order_count</code> is <code>COUNT(DISTINCT order_id)</code> at line grain, and <code>category</code> is a dimension on the same table.</p>
<p>When I run:</p>
<pre><code class="language-bash">mf query --metrics average_order_value --group-by category
</code></pre>
<p>It returns numbers. Outerwear $110, Socks $13, and so on. No error, no warning, no indication that a choice was made.</p>
<p>What it actually computed: for each category, sum the net revenue of lines in that category, divided by the count of distinct orders that touched that category. That's method 2 from the list above — line-level revenue divided by order-level counts. The denominator counts the multi-category order in both Outerwear and Accessories.</p>
<p>The calculation is mathematically valid. The problem is its interpretation.</p>
<p>It measures something like:</p>
<blockquote>
<p>category-line revenue per order that contains this category</p>
</blockquote>
<p>That could be a useful metric. It is simply not the only defensible interpretation of "average order value by category."</p>
<p>Nothing in the output signals that an attribution or definitional choice has been made.</p>
<h3>Was this caused by bad data modeling?</h3>
<p>This is where the distinction between data modeling and semantic modeling matters.</p>
<p><code>fct_order_items</code> at line-item grain is not inherently a bad model. In fact, it is a natural place to represent product, category, quantity, and line-level revenue.</p>
<p>The problem is that the requested metric and dimension naturally live at different conceptual grains:</p>
<ul>
<li><p><strong>AOV</strong> is fundamentally an order-level concept.</p>
</li>
<li><p><strong>Category</strong> is a line-item-level concept.</p>
</li>
<li><p>An order can contain multiple categories.</p>
</li>
</ul>
<p>A different data model could make one particular definition explicit.</p>
<p>For example, we could create a category-order fact such as:</p>
<pre><code class="language-text">order_id
category
attributed_revenue
</code></pre>
<p>Then <code>category AOV</code> could be defined over that model.</p>
<p>But that doesn't make the ambiguity disappear. We would still have to decide whether <code>attributed_revenue</code> means full-order attribution, line revenue, proportional allocation, or something else.</p>
<p>Better data modeling can <strong>encode an attribution decision</strong>.</p>
<p>It cannot determine <strong>which attribution decision the user intended</strong>.</p>
<p>That's why I don't think the right conclusion is "the semantic layer should have been modeled differently." The deeper problem is that the question itself is underspecified.</p>
<h3>How I found this</h3>
<p>I originally had two semantic models — one at order grain, one at line-item grain. <code>order_count</code> lived on the order model, <code>category</code> lived on the line model. When I asked for AOV by category, MetricFlow refused: no valid join path from an order-grain measure to a line-grain dimension.</p>
<p>That refusal was accidental. I hadn't designed a guardrail — MetricFlow's join resolution just couldn't bridge the two grains for that combination. But it was correct behavior: the system couldn't compute the ambiguous thing, so it didn't.</p>
<p>Then I simplified to a single semantic model at line grain. Everything lives on one table, no cross-grain joins, simpler to maintain. The simplification was an improvement in every other way. But it removed the accidental guardrail, because <code>order_count</code> (now <code>COUNT(DISTINCT order_id)</code>) and <code>category</code> live on the same table. MetricFlow has no join to resolve, so it answers without hesitation.</p>
<p>I ran it, got numbers, and almost moved on. The numbers looked normal. That's the dangerous part.</p>
<h3>The semantic-layer gap</h3>
<p>This isn't a MetricFlow bug. The metric definition is valid, and the generated calculation is valid for the chosen interpretation.</p>
<p>What the experiment exposes is a gap in conventional semantic-layer abstractions.</p>
<p>Semantic layers are good at describing <strong>how</strong> a metric is computed: aggregation, filters, measures, dimensions, and relationships between entities.</p>
<p>But that doesn't necessarily tell the system <strong>whether a metric is unambiguous for a particular dimension</strong>.</p>
<p>For example, a semantic layer could theoretically know that:</p>
<pre><code class="language-text">average_order_value + month
    → unambiguous

average_order_value + category
    → requires an attribution policy
</code></pre>
<p>The challenge is that conventional metric definitions generally don't capture this kind of metric × dimension validity or attribution requirement.</p>
<p>That means a query can pass every technical check:</p>
<ul>
<li><p>the metric exists;</p>
</li>
<li><p>the dimension exists;</p>
</li>
<li><p>the relationship is valid;</p>
</li>
<li><p>the SQL is valid;</p>
</li>
<li><p>the aggregation is valid.</p>
</li>
</ul>
<p>And the result can still be semantically ambiguous.</p>
<p>That's a more interesting failure mode for GenBI than broken SQL.</p>
<p>Broken SQL usually announces itself.</p>
<p>A clean query that returns a plausible number for an underspecified business question does not.</p>
<h3>What I'm doing about it</h3>
<p>I'm not trying to "fix" the metric in the semantic layer. The metric is defined correctly. The computation is correct for one valid interpretation.</p>
<p>The problem is that the question has multiple valid readings and the system picks one without asking.</p>
<p>Instead, I'm putting this question into the <a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce">evaluation harness</a> I'm building. It's one of 20 "ambiguous tier" questions where the correct system behavior is to ask for clarification, not to answer. The harness will test multiple systems — including MetricFlow, a RAG-based agent, and raw text-to-SQL — on the same question, and measure which ones ask and which ones guess.</p>
<p>I also added a warning to the metric definition:</p>
<pre><code class="language-yaml">description: &gt;
  Net revenue per order. WARNING: grouping by category produces
  "average category-line revenue per order touching that category,"
  not true AOV within a category, because 28.62% of orders span
  multiple categories.
</code></pre>
<p>That warning ends up in the schema context that an LLM reads.</p>
<p>Whether it changes the model's behavior is something the eval will measure.</p>
<p>And that's an important distinction: if the warning makes the model ask for clarification, that's useful evidence that metadata can help. If the model still confidently generates the query, that tells me the problem needs a different mechanism than metric descriptions alone.</p>
<h3>Caveats</h3>
<p>One dataset, one semantic layer tool.</p>
<p>The 28.62% multi-category rate is specific to this snapshot — a different business might have 5% or 80%, which changes how material the issue is.</p>
<p>The point isn't the specific number.</p>
<p>It's that transactional data frequently contains relationships where a metric defined at one grain becomes ambiguous when grouped by a dimension at another grain.</p>
<p>Whether a semantic layer should encode these ambiguities, whether an agent should detect them dynamically, or whether they belong in an evaluation or governance layer is still an open design question.</p>
<p>That's what I'm testing next.</p>
<hr />
<p><em>Part of a project building an evaluation harness for AI-generated SQL.</em></p>
<p>Repo: <a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce">github.com/Sean-Liu-GitHub/genbi-ecommerce</a></p>
<p>Key files:</p>
<ul>
<li><p><a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce/blob/main/dbt_project/models/semantic/metrics.yml">Semantic layer definitions</a></p>
</li>
<li><p><a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce/blob/main/docs/DEFINITIONS.md">Business definitions</a></p>
</li>
<li><p><a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce/blob/main/docs/SCHEMA_NOTES.md">Schema profiling — Block 10 (category spanning)</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[The definitions document isn't documentation. It's an accuracy intervention]]></title><description><![CDATA[In a previous post I showed that column descriptions improve the accuracy of AI-generated SQL but don't make it deterministic. This is the follow-up: what happens when you go further and give the mode]]></description><link>https://zhaidata.com/the-definitions-document-isn-t-documentation-it-s-an-accuracy-intervention</link><guid isPermaLink="true">https://zhaidata.com/the-definitions-document-isn-t-documentation-it-s-an-accuracy-intervention</guid><category><![CDATA[AI]]></category><category><![CDATA[SQL]]></category><category><![CDATA[data-engineering]]></category><category><![CDATA[dbt]]></category><category><![CDATA[bigquery]]></category><dc:creator><![CDATA[Sean Liu]]></dc:creator><pubDate>Thu, 03 Sep 2026 02:46:43 GMT</pubDate><content:encoded><![CDATA[<p>In a previous <a href="https://zhaidata.com/better-column-descriptions-made-the-ai-right-more-often-not-reliably">post</a> I showed that column descriptions improve the accuracy of AI-generated SQL but don't make it deterministic. This is the follow-up: what happens when you go further and give the model a structured business definitions document?</p>
<p>Short answer: it fixed every error I tested, and — more interesting — it changed the <em>kind</em> of answer the model gave.</p>
<h3>Setup</h3>
<p>I built a dbt project over BigQuery's <code>thelook_ecommerce</code> dataset. The marts layer has two main tables: <code>fct_order_items</code> (line-item grain) and <code>fct_orders</code> (order grain, pre-aggregated). Both are clean, well-named, documented in the schema yaml.</p>
<p>Before building anything AI-related, I wrote a <a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce/blob/main/docs/DEFINITIONS.md">definitions document</a> — ten business decisions, each traceable to a <a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce/blob/main/docs/SCHEMA_NOTES.md">profiling finding</a>. Things like: what does "revenue" mean (gross or net)? Which statuses count as "sold"? Which timestamp anchors a time period? Which join path reaches the product table?</p>
<p>Then I asked Claude the same three questions, twice: once with just the schema, once with the definitions document pasted as context.</p>
<h3>Question 1: "How many items did we sell in 2023?"</h3>
<p><strong>Without definitions:</strong></p>
<pre><code class="language-sql">SELECT COUNT(*) AS items_sold
FROM fct_order_items
WHERE EXTRACT(YEAR FROM ordered_at) = 2023
  AND line_status != 'Returned'
</code></pre>
<p>It filtered out returns — reasonable. But it kept Cancelled items. In this dataset, a cancelled item isn't a sale. The model made a judgment call about which statuses count as "sold" and got it partially right.</p>
<p><strong>With definitions:</strong></p>
<pre><code class="language-sql">SELECT COUNT(*) AS items_sold
FROM fct_order_items
WHERE EXTRACT(YEAR FROM ordered_at) = 2023
  AND line_status NOT IN ('Returned', 'Cancelled')
</code></pre>
<p>Correct. And it cited the reasoning: "the definitions specify that Processing lines count as sales because the customer committed to buy."</p>
<p>The difference isn't just the filter. Without definitions, the model explained what it did. With definitions, it explained <em>why</em>, referencing a convention rather than improvising one.</p>
<h3>Question 2: "What was our revenue in 2023?"</h3>
<p>This is the question with two defensible answers.</p>
<p><strong>Without definitions:</strong></p>
<pre><code class="language-sql">SELECT SUM(gross_revenue) AS revenue_2023
FROM fct_orders
WHERE EXTRACT(YEAR FROM ordered_at) = 2023
  AND order_status != 'Cancelled'
</code></pre>
<p>Three problems:</p>
<ol>
<li><p>It picked <code>gross_revenue</code> — which includes returned items. In this dataset, returns are 10% of revenue. That's a material difference.</p>
</li>
<li><p>It added an ad-hoc <code>order_status != 'Cancelled'</code> filter on the header, but <code>gross_revenue</code> was already computed from all lines regardless of status. The filter and the column contradict each other.</p>
</li>
<li><p>It never mentioned that the table has a <code>net_revenue</code> column, or that the choice between gross and net existed.</p>
</li>
</ol>
<p><strong>With definitions:</strong></p>
<pre><code class="language-sql">SELECT SUM(net_revenue) AS revenue_2023
FROM fct_orders
WHERE EXTRACT(YEAR FROM ordered_at) = 2023
</code></pre>
<p>Correct. No ad-hoc filter needed — <code>net_revenue</code> already excludes Returned and Cancelled at the line level. And it explained why the previous approach was wrong: "adding <code>AND order_status != 'Cancelled'</code> would actually under-count by dropping entire orders that have a Cancelled header status."</p>
<p>This is the result that surprised me most. Without definitions, the model didn't just pick the wrong column — it built an inconsistent query that mixed a gross measure with a net filter. With definitions, it picked the right column and <em>caught the mistake it would have made</em>.</p>
<h3>Question 3: "What was our gross margin by category in 2023?"</h3>
<p><strong>Without definitions:</strong></p>
<pre><code class="language-sql">SELECT category, SUM(line_margin) AS gross_margin, ...
FROM fct_order_items
WHERE EXTRACT(YEAR FROM ordered_at) = 2023
GROUP BY category
</code></pre>
<p>No status filter. Margin included returned and cancelled items. The number is defensible in isolation — you could argue margin should include all items — but it's inconsistent with how the same model answered question 1, where it filtered out returns.</p>
<p>That inconsistency is the real problem. Across three questions, the model without definitions made three different decisions about what "sold" means. A user asking these questions in sequence would get numbers that don't reconcile with each other.</p>
<p><strong>With definitions:</strong></p>
<pre><code class="language-sql">SELECT category, SUM(line_margin) AS gross_margin, ...
FROM fct_order_items
WHERE EXTRACT(YEAR FROM ordered_at) = 2023
  AND line_status NOT IN ('Returned', 'Cancelled')
GROUP BY category
</code></pre>
<p>Correct filter, consistent with questions 1 and 2.</p>
<h3>The pattern</h3>
<p>Without definitions, the model was:</p>
<ul>
<li><p>Partially right on status filtering (caught returns, missed cancellations)</p>
</li>
<li><p>Inconsistent across questions (filtered status for counts, not for margin)</p>
</li>
<li><p>Silent about ambiguity (never mentioned that "revenue" has two meanings)</p>
</li>
<li><p>Incoherent within a single query (gross column + net filter on Q2)</p>
</li>
</ul>
<p>With definitions, the model was:</p>
<ul>
<li><p>Correct on every status filter</p>
</li>
<li><p>Consistent across all three questions</p>
</li>
<li><p>Explicit about conventions ("per the business definitions...")</p>
</li>
<li><p>Self-correcting ("this differs from my earlier draft because...")</p>
</li>
</ul>
<p>The definitions document didn't just improve accuracy. It gave the model a coherent framework to reason from, and the reasoning was visible in the response.</p>
<h3>What a definitions document actually is</h3>
<p>This is the point I want to make clearly, because it changes how you think about the work.</p>
<p>Most teams treat business definitions as documentation — something you write for onboarding or compliance, stored in Confluence, read once, and never updated. The definition of "active user" is written somewhere, and nobody checks whether the dashboard agrees with it.</p>
<p>What this experiment suggests is that a definitions document is an <em>accuracy intervention</em> — it directly changes the correctness of AI-generated queries, measurably, on every question that touches a defined term. That makes it engineering work, not paperwork. And it makes maintaining it operational, not optional.</p>
<p>The ten definitions I wrote took about two days, including the <a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce/blob/main/scripts/profile.sql">profiling</a> that produced the evidence behind each one. Every one resolved an ambiguity that would otherwise be resolved differently on every query, by every user, with no way to detect the inconsistency.</p>
<h3>Caveats</h3>
<p>Three questions, one model (Claude), tested 2026-09-03. I didn't run N repetitions on this one because I used the chat interface rather than the API — so I can't report variance. The findings are directional, not statistical. The full eval harness with systematic runs across multiple models is coming.</p>
<p>This also tested the ideal case: the definitions were pasted directly into context. In a real deployment, the definitions would need to be retrieved, selected, and injected automatically — which is a harder problem. What this shows is the ceiling: <em>if</em> the model has the definitions, it uses them well.</p>
<hr />
<p><em>Part of a project building an evaluation harness for AI-generated SQL. The definitions document, the profiling methodology, and the dbt project are open-source.</em></p>
<p>Repo: <a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce">github.com/Sean-Liu-GitHub/genbi-ecommerce</a></p>
<p>Key files:</p>
<ul>
<li><p><a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce/blob/main/docs/DEFINITIONS.md">Business definitions</a></p>
</li>
<li><p><a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce/blob/main/docs/SCHEMA_NOTES.md">Schema profiling notes</a></p>
</li>
<li><p><a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce/blob/main/scripts/profile.sql">Profiling SQL</a></p>
</li>
<li><p><a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce/blob/main/dbt_project/thelook/models/intermediate/int_order_items_enriched.sql">The intermediate model that resolves the join path</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Better column descriptions made the AI right more often. Not reliably.]]></title><description><![CDATA[I assumed most "the AI can't query our warehouse" problems are really documentation problems. Before building an evaluation harness to test that properly, I ran a small experiment.
Half right, it turn]]></description><link>https://zhaidata.com/better-column-descriptions-made-the-ai-right-more-often-not-reliably</link><guid isPermaLink="true">https://zhaidata.com/better-column-descriptions-made-the-ai-right-more-often-not-reliably</guid><category><![CDATA[data-engineering]]></category><category><![CDATA[BUSINESS INTELLIGENCE ]]></category><category><![CDATA[AI]]></category><category><![CDATA[SQL]]></category><category><![CDATA[data analytics]]></category><dc:creator><![CDATA[Sean Liu]]></dc:creator><pubDate>Fri, 28 Aug 2026 06:27:19 GMT</pubDate><content:encoded><![CDATA[<p>I assumed most "the AI can't query our warehouse" problems are really documentation problems. Before building an evaluation harness to test that properly, I ran a small experiment.</p>
<p>Half right, it turns out. The half that's wrong is the interesting bit.</p>
<h2>Setup</h2>
<p>I snapshotted BigQuery's public <code>thelook_ecommerce</code> dataset into my own project on 2026-08-27. Synthetic ecommerce data — orders, line items, products, inventory, users. Clean by the standards of a real warehouse, which matters: whatever breaks here breaks worse in production.</p>
<p>One question, asked repeatedly through Gemini in BigQuery:</p>
<blockquote>
<p>How many items did we sell in 2023?</p>
</blockquote>
<p>10 runs per configuration, changing exactly one thing between configurations. No prompt engineering, no hints. Just the question a business user would type.</p>
<p>I want to be precise about what kind of failure I was looking for. A model that writes broken SQL is easy to catch — the query fails, you see it immediately. The harder failure is when the SQL is valid, the number is plausible, and the model has silently made a business decision on your behalf. That's what this experiment is about.</p>
<p>The <code>order_items</code> table is the one that matters. Here's its schema in BigQuery — note the <code>status</code> column:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a7a822667e5903ca79d87c7/2b37cacd-a21c-46a5-867e-25663e633556.png" alt="order_items schema in BigQuery" style="display:block;margin:0 auto" />

<h2>The trap</h2>
<p><code>order_items.status</code> holds values including Complete, Shipped, Processing, Cancelled and Returned. Nothing in the schema says which of those count as a sale.</p>
<p>29.2% of lines in this dataset are returned. So "how many items did we sell" has at least two defensible answers, and they differ by 29.2%.</p>
<p>This isn't a bug. It's ordinary warehouse ambiguity — the kind that lives in every schema I've worked on, usually resolved by an analyst who happens to know.</p>
<h2>Run 1: no description</h2>
<p>Hit rate: <strong>0/10</strong></p>
<p>It never filtered on status. Every run counted returned and cancelled items as sales.</p>
<pre><code class="language-sql">-- how many items did we sell in 2023
SELECT
  COUNT(DISTINCT order_items.id) AS count_of_items_sold
FROM
  `genbi-ecommerce`.`raw_thelook`.`order_items` AS order_items
WHERE
  TIMESTAMP_TRUNC(order_items.delivered_at, YEAR) = TIMESTAMP '2023-01-01 00:00:00 UTC';
</code></pre>
<p>The SQL was valid. The number was plausible. The question genuinely doesn't specify whether returns count. The model made a business decision and presented the result as a fact — and that's a worse failure than broken SQL, because broken SQL announces itself and this doesn't.</p>
<h2>Run 2: a vague description</h2>
<p>I added a description for the <code>status</code> column on the <code>order_items</code> table:</p>
<pre><code class="language-sql">ALTER TABLE `project.raw_thelook.order_items`
ALTER COLUMN status SET OPTIONS(
  description="Sales metrics only include complete items by default."
);
</code></pre>
<p>Hit rate: <strong>6/10</strong></p>
<p>Better, and unstable. Sometimes it filtered correctly. Sometimes it ignored the description entirely. Same question, same schema, same description — different answers across runs.</p>
<pre><code class="language-sql">-- a run where it filtered correctly
SELECT
  COUNT(order_items.id) AS count_of_items_sold
FROM
  `genbi-ecommerce`.`raw_thelook`.`order_items` AS order_items
WHERE
  order_items.status = 'Complete'
  AND TIMESTAMP_TRUNC(order_items.created_at, YEAR) = TIMESTAMP '2023-01-01 00:00:00';
</code></pre>
<p>In the other 4 runs, it reverted to counting all rows — sometimes even querying a different table entirely. Looking at the description again, it was bad. "Complete items" asks the model to infer two things: that I meant <code>status = 'Complete'</code>, and that this excludes returns. Either inference can go the wrong way on any given run.</p>
<h2>Run 3: a precise description</h2>
<p>I made the description more precise:</p>
<pre><code class="language-sql">ALTER COLUMN status SET OPTIONS(
  description="Line-item fulfilment status. Values: Complete, Shipped, Processing,
  Cancelled, Returned. Sales and revenue metrics count only lines with
  status = 'Complete'; exclude Cancelled and Returned."
);
</code></pre>
<p>Hit rate: <strong>9/10</strong></p>
<pre><code class="language-sql">-- how many items did we sell in 2023
SELECT
  COUNT(DISTINCT order_items.id) AS count_of_items_sold
FROM
  `genbi-ecommerce`.`raw_thelook`.`order_items` AS order_items
WHERE
  order_items.status = 'Complete'
  AND EXTRACT(YEAR FROM order_items.delivered_at) = 2023;
</code></pre>
<p>Listing the actual values and using imperative phrasing helped measurably.</p>
<p>This is the finding I didn't expect. <strong>Description quality matters, not just description presence.</strong> If you're about to spend a sprint filling in your data catalog, that distinction is worth knowing before you start — a catalog full of one-line summaries may buy you much less than the effort suggests.</p>
<h2>The part that still bothers me</h2>
<p>Even at the best configuration, it wasn't 10/10.</p>
<p>A description is advisory. It shifts a probability. It doesn't remove an option. Nothing in the mechanism makes the correct filter mandatory, so some fraction of the time the model weighs it differently and skips it.</p>
<p>The status filter isn't the only thing that shifted between runs. The model also varied which timestamp column it used — <code>delivered_at</code> in some runs, <code>created_at</code> in others. That's another business-logic choice being made silently, and another axis of non-determinism that a description on a single column can't fix.</p>
<p>And the variance is a bigger problem than the failure rate. A system that's consistently wrong is fixable — you find the bug, you fix it. A system that's right 9 times out of 10 can't be trusted <em>even when it's right</em>, because there's no way to tell which run you're looking at. Someone spot-checks it once, sees a correct answer, and draws exactly the wrong conclusion about reliability. This is why I'm running every configuration 10 times rather than once.</p>
<h2>Two things I noticed but haven't measured</h2>
<p><code>order_items</code> carries both a <code>product_id</code> and an <code>inventory_item_id</code>, so there are two join paths to the same product. Across these runs the model sometimes took one and sometimes the other. I replaced the raw tables with a single curated view where the joins are already made, and that variance went away — there was no second path to choose. But I haven't counted it properly, and I haven't verified whether the two paths return different numbers.</p>
<p>Separately, the timestamp column variance I mentioned above needs its own controlled test. It's a business rule that should be defined somewhere the model can find it, and it's the kind of thing a semantic layer is designed to solve.</p>
<p>Both are separate experiments. I'll post them when I've run them.</p>
<h2>What I think this means</h2>
<p>If you're evaluating GenBI tools, the interesting question isn't "can it write SQL." It's "what happens when the question is under-specified," which is most real questions.</p>
<p>Documentation is worth doing, and doing precisely. It also isn't sufficient on its own. My working hypothesis — which is what the rest of this project exists to test — is that the last gap closes by not letting the model make certain choices at all: routing defined-metric questions to compiled SQL, and having the system ask rather than guess when a question is genuinely ambiguous.</p>
<p>I'll find out whether that's true and post the numbers either way.</p>
<h2>Caveats</h2>
<p>One question. One dataset. One trap. One model, tested 2026-08-28, 10 runs per configuration. Gemini in BigQuery is doing something genuinely hard here — inferring business meaning from a schema that doesn't contain it — and these tools change month to month. Treat this as a snapshot, not a verdict.</p>
<hr />
<p><em>Part of a project building an evaluation harness for AI-generated SQL on a dbt semantic layer. Repo and full methodology to follow. Repo:</em> <a href="https://github.com/Sean-Liu-GitHub/genbi-ecommerce"><em>github.com/Sean-Liu-GitHub/genbi-ecommerce</em></a></p>
]]></content:encoded></item><item><title><![CDATA[What Took BI Engineers Hours, WrenAI Did in 15 Minutes — A Hands-On Test with Manufacturing Data]]></title><description><![CDATA[As a data engineer with years of experience building pipelines and dashboards, I've always felt the traditional BI workflow could be faster: connecting data sources, writing SQL, designing visualizati]]></description><link>https://zhaidata.com/genbi-wren-ai-manufacturing-dashboard-15-minutes</link><guid isPermaLink="true">https://zhaidata.com/genbi-wren-ai-manufacturing-dashboard-15-minutes</guid><category><![CDATA[data-engineering]]></category><category><![CDATA[BUSINESS INTELLIGENCE ]]></category><category><![CDATA[AI]]></category><category><![CDATA[SQL]]></category><category><![CDATA[data analytics]]></category><dc:creator><![CDATA[Sean Liu]]></dc:creator><pubDate>Thu, 13 Aug 2026 03:32:52 GMT</pubDate><content:encoded><![CDATA[<p>As a data engineer with years of experience building pipelines and dashboards, I've always felt the traditional BI workflow could be faster: connecting data sources, writing SQL, designing visualizations, tweaking layouts, iterating with stakeholders.</p>
<p>So when I heard about GenBI (Generative Business Intelligence) — the idea that AI can generate dashboards from natural language — I was skeptical. I decided to put it to the test with a real dataset and a real use case.</p>
<h2>The Dataset: Manufacturing Downtime</h2>
<p>I used the <a href="https://mavenanalytics.io/data-playground/manufacturing-downtime">Manufacturing Downtime dataset</a> from Maven Analytics, a well-known practice dataset that simulates a soda bottling production line. It contains four tables:</p>
<ul>
<li><p><strong>Line Productivity</strong> — a fact table with details for each batch produced, including operator, product, and start/end times</p>
</li>
<li><p><strong>Products</strong> — a dimension table with product details like flavor and size</p>
</li>
<li><p><strong>Line Downtime</strong> — a fact table recording downtime in minutes by factor for each batch</p>
</li>
<li><p><strong>Downtime Factors</strong> — a dimension table categorizing each downtime factor (e.g., machine failure, operator error, inventory shortage)</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/6a7a822667e5903ca79d87c7/271aa4cd-35d3-4815-9012-1d4493ba3dee.png" alt="image" style="display:block;margin:0 auto" />

<p>The business questions I wanted to answer were straightforward but typical of what a manufacturing operations team would ask:</p>
<ol>
<li><p>What are the leading factors causing downtime?</p>
</li>
<li><p>Which operators are experiencing the most downtime?</p>
</li>
<li><p>Are certain operators struggling with specific types of issues?</p>
</li>
</ol>
<h2>The Traditional Approach</h2>
<p>In a traditional BI workflow, answering these questions means exploring the schema, figuring out that the downtime table needs to be unpivoted first, writing SQL joins across all four tables, building aggregations, choosing chart types, and arranging everything into a coherent dashboard. For a seasoned BI engineer, this typically takes 2–4 hours. For someone less experienced, it could take a full day.</p>
<p>But the bigger problem isn't speed — it's that <strong>the knowledge isn't transferable</strong>. The metrics live in SQL queries, in DAX formulas, or in a dbt semantic layer. The business logic lives in the engineer's head. Some good data teams write documentation, but it still takes time for a new analyst to internalize it. And every time a stakeholder requests a new dashboard, the whole process starts over.</p>
<p>This is where a semantic layer changes the equation — not just by speeding things up, but by making the business context explicit, reusable, and queryable by anyone.</p>
<h2>The Wren AI Approach: From Excel to Dashboard in 15 Minutes</h2>
<p>Here's where things got interesting. Wren AI is an open-source GenBI engine that lets you connect a data source, define a semantic model, and then ask questions in natural language. It generates SQL, runs it, and produces visualizations.</p>
<h3>Setting Up the Project</h3>
<p>The dataset is a single Excel file with a CSV data dictionary — that's it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a7a822667e5903ca79d87c7/d61d5a37-2294-49a0-a524-4cc7f42dee54.png" alt="image" style="display:block;margin:0 auto" />

<p>I asked Claude Code to set up the Wren AI project by pointing it at the <a href="https://docs.getwren.ai/oss/get_started/quickstart">quickstart guide</a>:</p>
<blockquote>
<p>Read the quickstart guide in this url <a href="https://docs.getwren.ai/oss/get%5C_started/quickstart">https://docs.getwren.ai/oss/get\_started/quickstart</a>. Guide me how to set up a Wren AI project for the Excel file in this folder.</p>
</blockquote>
<p>Claude Code handled the entire setup: created a Python environment, installed Wren AI and DuckDB, loaded all four Excel sheets into a DuckDB database, installed the Wren CLI skill, created a database profile, and scaffolded the project structure.</p>
<p>The most notable thing: <strong>Claude automatically detected that the Line Downtime table was in wide/pivoted format and unpivoted it</strong> into a normalized <code>(batch, factor, downtime_minutes)</code> structure — a data modeling decision that would normally require human judgment.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a7a822667e5903ca79d87c7/77d10f86-8c76-40ea-83bd-e56cd152be95.png" alt="image" style="display:block;margin:0 auto" />

<h3>Setting Up the Semantic Layer (5–10 minutes)</h3>
<p>The semantic layer is where you tell the AI what "downtime" means, how tables relate, and what metrics matter. Think of it as teaching the AI your business context.</p>
<p>Using the <code>/wren</code> skill, Claude Code generated a metadata YAML file for each table and a relationships file defining how they connect:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a7a822667e5903ca79d87c7/1b8e8f89-f5e3-4d1f-b26e-8842fb5f67d8.png" alt="image" style="display:block;margin:0 auto" />

<p>For example, the downtime factors table:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a7a822667e5903ca79d87c7/69065b2b-d07e-4bc5-a5f2-8340943b7623.png" alt="image" style="display:block;margin:0 auto" />

<p>And the relationships between all four models:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a7a822667e5903ca79d87c7/7b6fd2f0-22ea-43a0-97eb-2e722af86708.png" alt="image" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/6a7a822667e5903ca79d87c7/d3b295c4-b3a5-451b-92ca-fd106a811f68.png" alt="image" style="display:block;margin:0 auto" />

<h3>Asking Questions</h3>
<p>Once the semantic model was in place, I simply asked natural language questions.</p>
<p><em>"What are the top downtime factors by total minutes?"</em></p>
<blockquote>
<p>Machine adjustment is the biggest downtime factor at 332 total minutes, followed by Machine failure (254) and Inventory shortage (225). Notably, the top contributor is an operator error, but the next two are not — so downtime splits roughly evenly between operator-caused and non-operator causes.</p>
</blockquote>
<details>
<summary>View the generated SQL and results</summary>
<pre><code class="language-sql">SELECT df.description, df.operator_error, SUM(ld.downtime_minutes) AS total_minutes
FROM line_downtime ld
JOIN downtime_factors df ON ld.factor = df.factor
GROUP BY df.description, df.operator_error
ORDER BY total_minutes DESC
</code></pre>
<pre><code>       description operator_error total_minutes
Machine adjustment            Yes           332
   Machine failure             No           254
Inventory shortage             No           225
      Batch change            Yes           160
Batch coding error            Yes           145
             Other             No            74
     Product spill            Yes            57
 Calibration error            Yes            49
    Labeling error             No            42
      Label switch            Yes            33
 Conveyor belt jam             No            17
</code></pre>
</details>

<p><em>"Show me total downtime by operator."</em></p>
<blockquote>
<p>Charlie leads with 384 total downtime minutes across 17 events, closely followed by Dee (370 min, 19 events). Mac and Dennis are lower overall, but Mac has the highest minutes-per-event average (~25.5 min/event vs Dee's ~19.5), suggesting fewer but longer downtime incidents.</p>
</blockquote>
<details>
<summary>View the generated SQL and results</summary>
<pre><code class="language-sql">SELECT lp.operator, SUM(ld.downtime_minutes) AS total_minutes,
       COUNT(ld.downtime_minutes) AS downtime_events
FROM line_downtime ld
JOIN line_productivity lp ON ld.batch = CAST(lp.batch AS TEXT)
GROUP BY lp.operator
ORDER BY total_minutes DESC
</code></pre>
<pre><code>operator total_minutes  downtime_events
 Charlie           384               17
     Dee           370               19
     Mac           332               13
  Dennis           302               12
</code></pre>
</details>

<p>Wren AI generated correct SQL and returned results within seconds for each question.</p>
<h3>Adding a Cube (Optional)</h3>
<p>A cube is a semantic aggregation object: a model plus declared measures, dimensions, and time grains. It's optional, but useful when you want pre-defined, reusable aggregations — so everyone agrees on what "total downtime" means and can slice it without writing SQL.</p>
<p>My prompt:</p>
<blockquote>
<p>Use the /wren skill to add a cube named "downtime" over the line_downtime model, total_downtime_minutes as sum(downtime_minutes), broken down by operator, factor and product</p>
</blockquote>
<p>This created a view that pre-joins all four tables, and a cube definition on top of it:</p>
<pre><code class="language-yaml">name: downtime
base_object: downtime_detail
measures:
  - name: total_downtime_minutes
    expression: SUM(downtime_minutes)
    type: BIGINT
dimensions:
  - name: operator
    expression: operator
    type: TEXT
  - name: factor
    expression: factor_description
    type: TEXT
  - name: product
    expression: product
    type: TEXT
</code></pre>
<details>
<summary>View the underlying view definition</summary>
<pre><code class="language-yaml">name: downtime_detail
properties:
  description: "Pre-joined view of downtime events with operator, product, and factor details"
statement: |
  SELECT
    ld.batch, ld.factor, ld.downtime_minutes,
    df.description AS factor_description, df.operator_error,
    lp.operator, lp.product, lp.date,
    p.flavor, p.size
  FROM line_downtime ld
    JOIN downtime_factors df ON ld.factor = df.factor
    JOIN line_productivity lp ON ld.batch = CAST(lp.batch AS TEXT)
    JOIN products p ON lp.product = p.product
</code></pre>
</details>

<p>Now anyone can query the cube directly:</p>
<pre><code class="language-bash">wren cube query --cube downtime --measures total_downtime_minutes --dimensions operator,factor
</code></pre>
<h3>Building a Dashboard</h3>
<p>With the cube in place, I asked Wren AI to generate a dashboard:</p>
<blockquote>
<p>Use the /wren skill to build a GenBI dashboard from the downtime cube: downtime, filterable by product, operator and factor. Then preview it locally.</p>
</blockquote>
<p>The result: a local preview dashboard with KPI cards (total downtime: 1,388 min, average per event: 23 min), bar charts by operator, product, and factor, dropdown filters, and a sortable detail table.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a7a822667e5903ca79d87c7/6307e89a-5d14-4009-9a8c-75bc1ffb2e2e.png" alt="Vercel screenshot" style="display:block;margin:0 auto" />

<p><strong>Total: ~15 minutes</strong> from a raw Excel file to an interactive dashboard.</p>
<h2>What Impressed Me</h2>
<p><strong>The semantic layer makes the difference.</strong> This is what separates Wren AI from a simple "GPT wrapper over SQL." The semantic model — built using Wren's Modeling Definition Language (MDL) — gives the AI a structured understanding of data relationships and business terms. Without it, the AI would be guessing at joins and column meanings. With it, the results were accurate and contextually appropriate.</p>
<p><strong>It handled follow-up questions naturally.</strong> After seeing the top downtime factors, I could ask <em>"Break this down by operator"</em> and the AI understood the context. This conversational flow is something traditional BI tools simply don't offer.</p>
<h2>What You Should Know (The Honest Part)</h2>
<p><strong>The semantic model setup is not optional.</strong> If you skip it or define it poorly, output quality drops significantly. Someone with data modeling experience still needs to be involved — GenBI doesn't eliminate the need for BI engineers, it redirects their effort from writing queries to defining business context.</p>
<p><strong>Chart customization is minimal.</strong> You can't control colors, axes, or layout the way you would in Power BI or Tableau. But that's by design: Wren AI prioritizes speed of insight over presentation polish. If your stakeholders need pixel-perfect branded dashboards, you'd use Wren AI for exploration and hand off the final presentation to a traditional BI tool.</p>
<p><strong>Complex business logic needs guidance.</strong> For straightforward aggregations and breakdowns, Wren AI performed well. For more complex calculations — like efficiency ratios or time-weighted averages — you'd need to define those as metrics in the semantic layer rather than expecting the AI to infer them.</p>
<h2>The Bigger Picture</h2>
<p>This isn't about replacing BI engineers — it's about changing what they spend their time on. Instead of writing repetitive aggregation queries and building routine dashboards, they can focus on the semantic layer: defining business logic, ensuring data quality, and building the foundation that makes GenBI reliable. For manufacturing teams specifically, downtime analysis shouldn't require a half-day turnaround. An operations manager should be able to ask <em>"Why did Line 3 have so much downtime last week?"</em> and get an answer in seconds.</p>
<hr />
<p><em>I'm a data engineer exploring how GenBI is reshaping the BI landscape. I write about hands-on experiences with tools like Wren AI and Databricks Genie. Follow me for more practical takes on the future of data analytics.</em></p>
]]></content:encoded></item></channel></rss>