Decision Spine
Research
Agentic Analytics13 min read

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.

By Dmitry Ustimov

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.

From a question to an answer
1 · Can it be answered at all?the previous article2 · Is every parameter grounded?this articlenomatchmatchQuestion“app opens, iPhoneusers in Germany,summer 2026”Catalogue searchmatch a metric nameOntology graphexists? joinable?Governedan approved metricComputabledata there, undefinedUninstrumentedthe data is not thereGround everyparameterby a rule ora dimensionsearchAnswerevery parameteris grounded; anyassumption statedAsk, or definea parameter hasno source, or tworeadings
The shaded section finds a governed metric. Next, the analyst checks each parameter's source, including the metric's exclusions. It answers with stated assumptions, or asks for clarification when a parameter is unresolved.

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 analyst writes a call, not SQL

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,
)
The analyst selects a metric and sets its parameters. The semantic layer generates SQL using the metric's definition. Only the metric is required; the optional parameters change the result.

These are the dimensions and values available when querying app_opens in our experiment:

Available dimensions for app_opens
dimensionvaluesused as
platformandroid · ios · web · unknownfilter or group by
device_typemobile · desktop · unknownfilter or group by
country_nameUnited States · Brazil · United Kingdom · Germany · France · Philippines · Indonesia · Indiafilter or group by
regionAmericas · EMEA · APACfilter or group by
hemispherenorth · southfilter or group by
channelcontent_seo · organic · paid_search · partnerships · referralfilter or group by
time5 September 2025 to 12 July 2026period or time grain
is_internalfalsefixed by the metric
Filters select dimension values; group by uses dimension names. The period selects dates, and time grain groups them. Internal accounts are always excluded by the metric's definition.

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:

CallApp 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.

One question, four parameters

How many app opens came from iPhone users3 in Germany1 in summer 20262?not said: internal accounts4

  1. 1Germanydimension search · exact matchone of the values of country_namecountry_name = Germany
  2. 2summer 2026a rulethe company calendar; data ends 12 July2026-06-01 to 2026-07-12
  3. 3iPhone usersdimension search · AI analyst judgementno exact match; an iPhone runs iOSplatform = ios
  4. 4not saida rulethe metric's definitionis_internal = false
The question's parameters in the order the analyst settles them, each with the source it comes from. The fourth is never said in the question.

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.

AI analyst judgement, as a multiple-choice question

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

One real answer from the experiment. Code builds the options from the layer's values; the model only gives a confidence for each. Code then applies two fixed checks. If either fails, the analyst asks.

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.

The grounded answer
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.
Every parameter has a source, and every assumption is written next to the number. Dropping “iPhone” would have given 5,860.

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 groundedIn the exampleWhat the analyst does
A rule the data team wrote downsummer; internal accountsAnswers, and names the rule
Dimension search: an exact matchGermanyAnswers
Dimension search: AI analyst judgementiPhone users as iosAnswers, and says how it read the words
Nothing grounds itsummer, for a question about BrazilAsks

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.

Inside a dimension search
Phrase“iPhone users”1 · NarrowCandidate searchusing the dimension’s methods:all · words · aliasesmeaning · pattern · hierarchyCandidatesios (platform)mobile (device type)…each shown with context2 · DecideResolutioncode looks for anexact match, themodel judges the restExact matchused as it is,with no note in the answerAI analyst judgementthe model is sure of one value,and the answer explains the choiceAsknothing fits well enough,so the analyst asks the userNot in the dataonly if every value was checkedand the list is completeFrom the semantic layervalues and their meanings · aliases · hierarchy · complete lists · search methods
The search finds the few values a phrase could mean. Code then looks for an exact match, and the model is asked only about the rest. What the search can use comes from the semantic layer.

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.

Search methods a dimension can declare
methodforhowexampleexhaustive
allshort lists, up to 50 values: platform, country, channelevery value becomes a candidate“Deutschland” → the 8 countriesyes
wordsnames where spelling is the risknormalised spelling distance“Muncih” → Munichno
aliasesplaces, companies, customersan alias table, then words“Bombay” → Mumbai, “Big Apple” → New York Cityno
meaningdescriptive categories, in any languagemultilingual embeddings, merged with words“running shoes” → athletic footwearno
patterncodes, IDs, SKUsa format rule, then exact or prefix match; never embeddings“SKU-10442”, not its neighbour SKU-10443yes, on a complete list
hierarchygeography, organisationsa parent named in the question limits the search to its subtree; otherwise the best two parents“Rome, Italy” is not Rome, Georgiayes, for a small complete subtree
Each method suits one way of naming a value. A dimension declares several, in priority order, and code runs them all. Only an exhaustive method can support “not in the data”.

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:

MetricWhat it measuresThe agent aloneLatest
Silent errorsa wrong number, given with nothing said30%1.4%
Balanced accuracythe right call on questions that should be answered and on those that should not, averaged63%95%
Coverageanswerable questions actually answered95%96%

We did not get there in one step. The experiment's findings record every change; five of them moved the numbers:

What we changedWhat 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.

The method in four steps
  1. 1

    List each metric's parameters

    Platform, country, period, time grain, group by: the columns of the grid below.

  2. 2

    Run real questions

    Questions people actually ask, written by someone outside the work.

  3. 3

    Mark each parameter

    Grounded, a stated assumption, asked back, or a silent error.

  4. 4

    Score each metric

    The share of its parameters grounded, and the number of silent errors.

Every step works on parameters, not on whole questions.

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.

A readiness grid (illustrative)
platformcountrychannelperiodtime graingroup bygrounding coverage
app_opens
7 grounded7 assumed1 ask
11 grounded9 assumed1 ask
5 grounded1 ask
52 grounded
6 grounded
19 grounded1 ask1 wrong
96%
116 of 121 parameters
active_users
9 grounded2 assumed1 wrong
10 grounded2 assumed2 asks
3 grounded2 asks
37 grounded3 asks
7 grounded2 asks
14 grounded2 asks
88%
84 of 96 parameters
signups
—
5 grounded1 assumed2 asks
8 grounded3 asks
24 grounded1 ask
2 grounded2 asks
9 grounded
86%
49 of 57 parameters
paying_users
6 grounded
4 grounded3 assumed
—
17 grounded1 ask
3 grounded
5 grounded
97%
38 of 39 parameters
revenue
—
8 grounded1 assumed
—
30 grounded
5 grounded1 ask
7 grounded
98%
51 of 52 parameters
marketing_spend
—
—
12 grounded2 assumed
20 grounded
5 grounded
8 grounded
100%
47 of 47 parameters
overall
93%
385 of 412 parameters
groundedstated assumptionasks backsilent wrongnot a parameter
Illustrative, based on our real run. Each cell counts the test questions that used that parameter. A cell shows its worst outcome. Coverage counts grounded parameters, stated assumptions included.

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:

  1. The terms people use. If people ask about "mobile", model it as a value they can query. Include aliases such as "Deutschland" for Germany.

  2. Write down the definitions and rules. For example: platform is the device an account registered on; "summer" means June to August in the company calendar; app_opens excludes internal accounts. The analyst needs these facts to choose the parameters and explain the number.

  3. 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.

  4. 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.

Read next

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.