What an AI Analyst Needs From Your Semantic Layer
A governed metric is not a governed answer. Every parameter of the query must be grounded in a source the analyst can point to. Here is what to put in your semantic layer, and how to measure whether it is ready.
An AI analyst that works on a semantic layer answers a question in two steps. First it chooses a governed metric. The previous article was about this step: the analyst finds the metric in the layer's catalogue, or learns that none exists. Then it fills in the details the question asks for: which platform, which country, which dates. This article is about the second step. It gets less attention, and it goes wrong more quietly. Every parameter of the query must be grounded.
Querying the semantic layer#
The semantic layer used in the experiment defines app_opens, the fields available for
filtering and grouping, and their values. The metric's definition always excludes internal accounts.
We give the agent a tool, query_metric, that queries this layer. The agent supplies the metric
and parameters; the layer generates SQL using the metric's definition.
The agent must set metric. It sets four optional parameters when the question requires them:
filters, group_by, period and time_grain. The tool accepts the layer's dimension names and
values, so the agent must translate the user's words into these arguments.
The sequence
AI analyst
reads the question, fills in the call
query_metric(…)
the metric and its parameters
Semantic layer
writes the SQL, applies the metric's definition
Warehouse
returns the number
The call’s arguments
- metricrequiredmetric nameapp_opens
- filtersoptionalkeep only these dimension values{platform: ios}
- group_byoptionalgroup the result by these dimensions[country_name]
- periodoptionaldate range2026-Q2 · 2026-03 · last_month · start/end
- time_grainoptionalday · week · month · quarter · year
query_metric( metric = app_opens, filters = {platform: ios}, group_by = [country_name], period = 2026-Q2, time_grain = week, )
These are the dimensions and values available when querying app_opens in our
experiment:
| dimension | values | used as |
|---|---|---|
| platform | android · ios · web · unknown | filter or group by |
| device_type | mobile · desktop · unknown | filter or group by |
| country_name | United States · Brazil · United Kingdom · Germany · France · Philippines · Indonesia · India | filter or group by |
| region | Americas · EMEA · APAC | filter or group by |
| hemisphere | north · south | filter or group by |
| channel | content_seo · organic · paid_search · partnerships · referral | filter or group by |
| time | 5 September 2025 to 12 July 2026 | period or time grain |
| is_internal | false | fixed by the metric |
The SQL can execute correctly even when an argument misreads the question. That is the failure this experiment tests.
Where it goes wrong#
Take one question: how many app opens came from iPhone users in Germany in summer 2026?
The metric is easy: app_opens. The parameters are not. "iPhone" is not a value of any dimension.
"Summer" is not a period the tool accepts. Nobody said anything about internal accounts. Here are two
calls an analyst could make:
| Call | App opens |
|---|---|
query_metric(app_opens, filters={country_name: Germany}, start=2026-06-01, end=2026-08-31) | 5,860 |
query_metric(app_opens, filters={platform: ios, country_name: Germany}, start=2026-06-01, end=2026-07-12) | 2,325 |
The first call drops "iPhone" because no value matches it. It runs, returns a clean number, and is two and a half times too large. Nothing fails: the metric is governed and the SQL is correct. The error is in a parameter. The second call is right, and it is still not the whole answer: the data ends on 12 July, so the answer must also say that summer is only partly covered.
The metric was governed. The answer was not.
Every parameter of the query must be grounded#
Go back to the iPhone question. A careful analyst does not reach for the nearest call. They take the question apart and settle one parameter at a time, from the easiest to the hardest. Each parameter is grounded in one of two ways: by a rule the data team wrote down, or by a dimension search that finds the value among the values of a dimension.
How many app opens came from iPhone users3 in Germany1 in summer 20262?not said: internal accounts4
- 1Germanydimension search · exact matchone of the values of country_namecountry_name = Germany
- 2summer 2026a rulethe company calendar; data ends 12 July2026-06-01 to 2026-07-12
- 3iPhone usersdimension search · AI analyst judgementno exact match; an iPhone runs iOSplatform = ios
- 4not saida rulethe metric's definitionis_internal = false
Germany is the easy one. The dimension search finds it as an exact match: it is one of the eight
values of country_name, so the parameter is country_name = Germany. There is nothing to interpret.
Summer needs a rule. No dimension has a value called summer, but the company calendar defines it as June to August. That definition only works north of the equator: for Brazil the same word means December to February, and the analyst would ask which months are meant. The layer records that Germany is in the north, so the rule applies and gives 1 June to 31 August 2026. One catch: the data stops on 12 July. The analyst answers for the six weeks it has and says that summer is only partly covered.
"iPhone" is where judgement comes in. No rule mentions it, and the dimension search finds no exact
match. Any analyst would read it as iOS, and platform = ios is the closest value on the menu. It is a little broader than the
question, since iOS also runs on iPads, which is why the answer spells out the choice. The layer adds a
second catch: platform is the device the account signed up on. Someone who signed up on an iPhone and
now opens the app on Android still counts as ios. The answer says that too.
The model is asked
In the question, what is “iPhone users”?
- the same thing as platform ios0.82
- narrower than platform ios0.13
- narrower than device type mobile0.02
- 71 other options, together0.02
Code checks
platform ios, as the same thing or narrower
0.82 + 0.13 = 0.95 ≥ 0.85 ✓
every other option, together
0.04 < 0.15 ✓
platform = ios
applied, and the answer states how “iPhone users” was read
The last parameter is one nobody said. The question never mentions internal accounts, but the
definition of app_opens in the layer excludes them. That does not change the call. It does change the
number, so the answer names it.
Now every parameter has a source the analyst can point to, and every assumption is written next to the number.
query_metric(
metric = app_opens,
filters = {
platform: ios,
country_name: Germany,
},
start = 2026-06-01,
end = 2026-07-12,
)2,325 app opens
- AI analyst judgementRead “iPhone users” as platform ios, which is broader: it includes more than iPhones.
- semantic modelPlatform means the device the account signed up on. App opens on other devices still count under it.
- a ruleSummer 2026 is read as June to August 2026 (the company calendar).
- semantic modelSummer 2026 is only partly in the data; this covers 1 June to 12 July 2026.
- semantic modelApp opens exclude internal accounts by definition.
That is what grounded means. A parameter is grounded by a rule or by a dimension search, and a dimension search ends in one of two ways:
| How the parameter is grounded | In the example | What the analyst does |
|---|---|---|
| A rule the data team wrote down | summer; internal accounts | Answers, and names the rule |
| Dimension search: an exact match | Germany | Answers |
| Dimension search: AI analyst judgement | iPhone users as ios | Answers, and says how it read the words |
| Nothing grounds it | summer, for a question about Brazil | Asks |
AI analyst judgement is allowed, because a senior analyst uses judgement every day. It is used only when no rule and no exact match settles the parameter, and it is always stated, because it is the step most likely to be wrong. The analyst never guesses silently.
How a dimension search works#
On the test layer a dimension search is easy. Every dimension has a short list, so the candidates are simply all of its values, and the model chooses among about 74 options. Real layers are not like that. Cities, products, customers and campaigns have thousands of values, and a question with thousands of options is slow, expensive and unreliable.
So a dimension search has two parts, with one contract between them. Candidate search narrows the values to a short list. Resolution decides. Code settles an exact match on its own. The model is asked only when no candidate matches exactly, and only about the candidates.
We cannot know how a user will name a value. The same city arrives as "Munich", "Muncih", "München", "MUC" or "Munich, Germany", and each form needs a different method. So a dimension declares several search methods, in priority order, and code runs all of them. They take milliseconds and need no model. An exact match from a high-priority method ends the search. Two different exact matches are a conflict, and the analyst asks. Otherwise the candidates from every method go to the model together, in one judgement.
| method | for | how | example | exhaustive |
|---|---|---|---|---|
| all | short lists, up to 50 values: platform, country, channel | every value becomes a candidate | “Deutschland” → the 8 countries | yes |
| words | names where spelling is the risk | normalised spelling distance | “Muncih” → Munich | no |
| aliases | places, companies, customers | an alias table, then words | “Bombay” → Mumbai, “Big Apple” → New York City | no |
| meaning | descriptive categories, in any language | multilingual embeddings, merged with words | “running shoes” → athletic footwear | no |
| pattern | codes, IDs, SKUs | a format rule, then exact or prefix match; never embeddings | “SKU-10442”, not its neighbour SKU-10443 | yes, on a complete list |
| hierarchy | geography, organisations | a parent named in the question limits the search to its subtree; otherwise the best two parents | “Rome, Italy” is not Rome, Georgia | yes, for a small complete subtree |
In the semantic layer, the declaration sits next to the values it searches:
- name: city
config:
meta:
search: [aliases, hierarchy, words, meaning] # in priority order
aliases: city_alias # Bombay, Big Apple, München
complete: true
Two rules keep the design safe.
Every candidate carries its context. A large dimension has namesakes. When we built a test city dimension of 12,409 cities, six of the foreign capitals we picked as test cases were already in it: Rome, Moscow, Lima, Dhaka, Vienna and Amsterdam, as smaller towns in the eight countries (Rome, Georgia; Lima, Ohio). An exact match on "Rome" would answer about Rome, Georgia, and the answer would look grounded. Shown as "Rome, Georgia, United States", the candidate can be judged, and the answer can state it.
"Not in the data" needs an exhaustive search. A short list from a similarity search always contains something, and a low score proves nothing. In the city test, the scores of absent cities overlapped the scores of misspelled present ones. The analyst may say a value is not in the data only when the dimension is declared complete, no exhaustive method found the phrase, and the model judges that it names a value missing from the list. Every other miss becomes a question.
Does it work?#
We built this in one experiment, changing the semantic layer and the analyst step by step. Each version was tested on fresh questions written outside the work: one AI model wrote the requests and the right outcome for each, another wrote three wordings of every request, neither saw our code, and we committed our predictions before each run. We tracked three numbers:
| Metric | What it measures | The agent alone | Latest |
|---|---|---|---|
| Silent errors | a wrong number, given with nothing said | 30% | 1.4% |
| Balanced accuracy | the right call on questions that should be answered and on those that should not, averaged | 63% | 95% |
| Coverage | answerable questions actually answered | 95% | 96% |
We did not get there in one step. The experiment's findings record every change; five of them moved the numbers:
| What we changed | What happened |
|---|---|
| 1. A separate step chooses each parameter from the values the layer allows. Before, the agent wrote the whole call at once. | Silent errors fell from 30% to 5%, but the analyst asked back on a quarter of the questions it could answer. |
2. A structured data model instead of prose. "Mobile means Android and iOS" was written only in a description; it became a device_type column. | Questions about "mobile" answered correctly rose from 25% to 100%. Before, the analyst mostly asked the user to clarify instead of answering. |
| 3. Code grounds each part by a rule or an exact match. Before, the AI decided which values a question meant. Now the AI only splits the question into parts, and code matches each part to a rule or a listed value. | On a new set of questions, silent errors fell from 12.5% to 0.9%, but coverage fell from 80% to 60%. The analyst asked whenever a word was not exactly on a list. |
| 4. AI analyst judgement where nothing matches. Before, the analyst asked about any word with no exact match, even "Deutschland". Now it reads "Deutschland" as Germany and says so in the answer. | Coverage rose from about 60% to 81%, with silent errors still near 1%. |
| 5. One check per decision. Each decision in the query gets its own yes/no question, such as "does the user want one number per week?" One check for the whole query blocked 83 correct queries to stop 13 wrong ones. | Coverage rose from 81% to 96%, and balanced accuracy from 82% to 95%. Silent errors stayed at 1.4%. |
How to measure your own semantic layer#
Before an AI analyst goes in front of your team, you want to know three things: where it will be right, where it will ask, and where it will give a wrong number with nothing said. A pass or fail per test question does not tell you that, and it does not tell you what to fix. So we measure the semantic layer itself, one parameter at a time: every parameter a metric accepts should have a source the analyst can find. The method checks that against the questions your people actually ask.
- 1
List each metric's parameters
Platform, country, period, time grain, group by: the columns of the grid below.
- 2
Run real questions
Questions people actually ask, written by someone outside the work.
- 3
Mark each parameter
Grounded, a stated assumption, asked back, or a silent error.
- 4
Score each metric
The share of its parameters grounded, and the number of silent errors.
The result is a grid. Each row is a metric, and each column is a parameter it accepts. Each cell counts what happened to the test questions that used that parameter: grounded, grounded on a stated assumption, asked back, or a silent error. The colour shows the worst outcome, so a single silent error turns a cell red. The last column shows, per metric, how many parameter checks were grounded. The counts are illustrative, modelled on our real run.
Read the red cells first: they are wrong numbers nobody noticed. Then the yellow ones: questions the analyst could have answered if the layer held one more value, meaning or rule.
| platform | country | channel | period | time grain | group by | grounding coverage | |
|---|---|---|---|---|---|---|---|
| app_opens | 7 grounded7 assumed1 ask | 11 grounded9 assumed1 ask | 5 grounded1 ask | 52 grounded | 6 grounded | 19 grounded1 ask1 wrong | 96% |
| active_users | 9 grounded2 assumed1 wrong | 10 grounded2 assumed2 asks | 3 grounded2 asks | 37 grounded3 asks | 7 grounded2 asks | 14 grounded2 asks | 88% |
| signups | — | 5 grounded1 assumed2 asks | 8 grounded3 asks | 24 grounded1 ask | 2 grounded2 asks | 9 grounded | 86% |
| paying_users | 6 grounded | 4 grounded3 assumed | — | 17 grounded1 ask | 3 grounded | 5 grounded | 97% |
| revenue | — | 8 grounded1 assumed | — | 30 grounded | 5 grounded1 ask | 7 grounded | 98% |
| marketing_spend | — | — | 12 grounded2 assumed | 20 grounded | 5 grounded | 8 grounded | 100% |
| overall | 93% | ||||||
What this means for your semantic layer#
I'd start with a few questions your team asks regularly. For each one, can the analyst find the right values, apply the right rules and explain its choices? A metric can be well defined and still return the wrong number if "iPhone users" is misinterpreted or "summer" uses the wrong dates.
Four things will help make your semantic layer ready for an AI analyst:
-
The terms people use. If people ask about "mobile", model it as a value they can query. Include aliases such as "Deutschland" for Germany.
-
Write down the definitions and rules. For example:
platformis the device an account registered on; "summer" means June to August in the company calendar;app_opensexcludes internal accounts. The analyst needs these facts to choose the parameters and explain the number. -
Help the analyst look up values. Specify how to search each dimension. For cities, return the state and country alongside the name, so "Rome" does not silently become Rome, Georgia. Also say whether the list contains every city in your data. Finding no match in a short list of suggestions does not prove that the city is absent from the full list.
I'm working on an architectural approach that makes search part of the semantic layer: each dimension defines how to find its values, and the agent uses that definition. Follow me for updates as I develop it.
-
Make evaluation a regular practice. Keep a set of real questions and rerun it whenever the layer or agent changes. Measure grounding for each parameter, rather than giving the whole question one pass or fail. That shows exactly what needs fixing: the country filter, the date range, the grouping or another part of the query.
The analyst should use those values and rules first. When it needs judgement, it should say how it read the question. If it cannot settle the meaning, it should ask.
Use the readiness grid to choose what to fix first: silent errors, then questions the analyst could answer if the layer supplied the missing meaning or rule.
Sources & further reading
- AI Analytics Harness — Dmitry Ustimov
- Experiment 07: semantic layer grounding — Dmitry Ustimov
- How an AI Analyst Knows What It Can Answer — Dmitry Ustimov, Decision Spine
- Semantic view YAML specification — Snowflake
- Genie best practices — Databricks
- Beyond Correctness: Resolving Underspecification in Agentic Text-to-SQL — Li et al., 2026
Read next
- Agentic Analytics20 min
What It Takes to Trust an AI Analyst
Lessons from six experiments on building reliable AI analytics: trusted data, governed definitions, a bounded verification harness, and evaluation that can see silent failure.
Read - Agentic Analytics10 min
How an AI Analyst Knows What It Can Answer
A closed-world ontology turns answerability from a model judgement into a system check: governed, computable but undefined, or unsupported by the warehouse.
Read - Agentic Analytics17 min
When Both Numbers Are Right
Product's governed active-user metric says 886. Platform's says 919. Both are correct. The experiment asks whether an AI analyst will expose the choice, and what it takes to make that reliable.
Read
Want this run against your own analytics environment?
The AI Analytics Reliability Audit runs these failure modes against your warehouse, your semantic layer, and your business questions, then hands you a prioritized repair plan.
