Decision Spine
Blog
Agentic Analytics14 min read

Why AI Analysts Pick the Wrong Metric

AI analysts fail before SQL generation when they choose the wrong metric, table, or column. Static checks can catch many of those wrong-choice traps before the agent runs.

By Dmitry Ustimov

Semantic sprawl is warehouse technical debt. A normal warehouse accumulates duplicate metrics, old names, unfinished migrations, hidden filters, stale columns, and undocumented flags. Nobody sets out to create ambiguity. It builds up because adding one more object is easy and deleting an old one is risky.

Wrong selection is the metric to watch. An AI analyst can write valid SQL and still answer the wrong business question. If it picks recurring_revenue when the governed definition is mrr, the query runs, the chart looks normal, and the number is wrong.

Static checks belong before agent evals. Golden evals are necessary, but they sample behavior. They do not cover every table, metric, column, alias, and hidden rule in the warehouse. A preflight scan can inspect the definitions first and remove obvious wrong choices before the expensive agent loop starts.

One question, three candidates
An AI analyst is asked: what is our current recurring revenue? Dashed arrows with question marks fan out to three metrics whose names all match the question: mrr, recurring_revenue, monthly_recurring_revenue. Nothing written says which one the company trusts.
The whole problem in one frame. The question matches three metric names; the agent must pick one; nothing written says which one the company trusts. Everything below is about making this choice impossible to get wrong.

Agents pick from the context you give them#

By AI analyst, I mean an LLM system that chooses warehouse objects, writes SQL, runs it, and returns an answer. That flow has two different failure modes.

Wrong construction is the familiar AI problem: the agent picked the right concept but built the query badly. Wrong join, wrong date window, wrong aggregation, missing filter. That is where better models, SQL validation, refusal rules, and verification guardrails help. I covered that in the guardrails article.

Wrong selection is quieter. The agent chose the wrong metric, table, column, or default before SQL generation began. The SQL may be correct for the thing it selected. The problem is that the thing it selected was not what the business meant.

Agent context is not just prompt text. It is the warehouse interface: metric names, table names, column docs, model structure, semantic definitions, defaults, and deprecations. If that interface shows three revenue metrics, six active-user aliases, and two user tables, the agent has to choose from a noisy context.

That is a warehouse problem. Bigger models can use clear context better. They cannot reliably recover company-specific meaning that is absent or contradictory.

One measure, two segments, shipped as two metrics
Two metric cards, value_moments and real_value_moments, share the same event, table and sum. The only difference is the segment: one counts everyone including staff, the other excludes internal accounts. Two bars below differ by about 4 percent: close enough that nobody notices.
Two metrics built from the same event and aggregation, split only by who gets counted. About 4% apart: small enough to look normal, large enough to change the answer.

I built a test warehouse around that failure. It is a small subscription product with five dbt-style models, documented columns, and a metrics file shaped like dbt's Semantic Layer example project. Then I gave it the problems I keep seeing in real companies.

The test warehouse
One warehouse, sprawling on both floors. The models: a current dim_users_v2 beside the legacy dim_users that stopped loading in July, two undocumented staff flags, a stale status column, and a copy named like a March partition that is really a stale copy of everything. The metrics layer: three revenue metrics, six active-user aliases, three value-moment variants, two signup counts, two habit counts, and a subscriber count that means something different from the paying one.
The test warehouse has versioned tables, undocumented flags, stale state, duplicate metric names, hidden filters, and one clean baseline: marketing_spend.

This is normal warehouse mess. Not a clean warehouse, not a disaster. It has old names beside new names, migrations that stopped at 80%, metrics that began as experiments and became permanent, and filters that everyone assumes someone else documented.

Selection errors happen in both layers. In the semantic layer, multiple metrics can compete for the same business question: mrr, recurring_revenue, and monthly_recurring_revenue. In the raw warehouse, the agent may still need tables and columns outside semantic-layer coverage: dim_users beside dim_users_v2, or is_internal beside is_test.

Semantic coverage is rarely complete. A semantic layer shrinks the choice surface when the agent is forced through it. Many real agentic setups are not that complete. The semantic layer covers common metrics, and the agent still falls back to raw SQL for questions outside that surface. That raw warehouse is still part of the context.

The quality-control pyramid#

You can find wrong selection with golden evals. Write a set of business questions, run the agent, compare each answer to a trusted SQL key, and inspect the traces when it fails. I do that below. It works.

But evals do not cover a warehouse. They cover a question set. Every extra metric, alias, table version, stale column, and hidden filter gives the agent another choice it could make. Testing all of those choices by running the agent is too slow and too expensive.

Quality-control pyramid

Runtime guardrails + review

narrow / expensive

Verify high-stakes answers, abstain when context is missing, and route sensitive cases to review.

Golden evals

targeted / slower

Run the agent on known business questions. Catch regressions in the workflows you care about most.

Static health checks

broad / cheap

Scan names and definitions across the whole project before the agent sees the warehouse.

Static checks are broad and cheap, so they belong at the base. Golden evals are narrower and slower, so they test the important workflows. Runtime guardrails and review sit at the top for high-stakes answers.

Read it from the bottom up:

  • Coverage first. Static checks can inspect every model, metric, and column definition.
  • Evidence next. Evals prove the agent behaves on the questions the business actually asks.
  • Protection last. Runtime guardrails and manual review handle cases that are too important to trust blindly.

Static checks are broad but shallow. Evals are narrow but behavioral. Guardrails are runtime and expensive. You need all three, but you do not want the expensive layers carrying obvious warehouse hygiene.

That is where preflight fits. It complements dbt tests, docs, and contracts. It does not check whether a model built successfully or whether a column is non-null. It checks a different failure mode: confusing definitions and names that can lead an agent to select the wrong valid object.

Static analysis for the warehouse#

Preflight works like a code linter. Instead of running the warehouse, it reads the project files: metric YAML, model YAML, and SQL. Then it uses names, structure, and embeddings to find definitions that are easy to confuse but would produce different answers. There is no agent run and no warehouse connection. The scan happens before runtime, on the definitions you already have.

It decides in three steps:

  1. Read every definition into one comparable shape: what is measured, from which table, over which rows, at what grain.
  2. Pair up the definitions a person could mix up: names that read alike, or meanings that overlap.
  3. Check each pair structurally: would these two return different numbers for the same question? If yes, that pair becomes a finding, with a severity and the file and line where each side is defined.

Pointed at the test warehouse, it printed this:

ONE WAREHOUSE — 11 findings (high 4, medium 3, low 4)

METRICS LAYER

HIGH
  [SCOPE_TRAP]  new_signups ~ new_users
      same measure; 'new_users' is 'new_signups' plus a filter — a bare
      question is silently scoped
  [SCOPE_TRAP]  value_moments ~ real_value_moments ~ total_moments
  [SCOPE_TRAP]  active_users ~ actives ~ dau ~ engaged_users ~ mau ~ monthly_active_users
  [FACT_TWIN]   paying_users ~ subscribers
      the same count over one process at two grains: the current book and
      every subscription ever

MEDIUM
  [FACT_TWIN]       active_habits ~ total_habits

LOW
  [DUPLICATE]   monthly_recurring_revenue ~ mrr        # one meaning, two names
  [DUPLICATE]   total_moments ~ value_moments
  [DUPLICATE]   actives ~ dau ~ engaged_users ~ mau ~ monthly_active_users
  [NAME_COLLISION]  monthly_recurring_revenue ~ recurring_revenue
      names read alike, definitions do not agree — different measure,
      different table

MODELS

MEDIUM
  [VERSIONED_TWIN]  dim_users ~ dim_users_v2
      'dim_users_v2' reads as a versioned or leftover twin — nothing marks
      which one is current
  [VERSIONED_TWIN]  fct_subscriptions ~ fct_subscriptions_2026_03

How to read it:

  • SCOPE_TRAP: two metrics share the same measure, but one has a filter the other does not. A plain question can land on the wrong population.
  • DUPLICATE: one meaning under two names. This is lower risk because the numbers should agree.
  • NAME_COLLISION: two names read alike while the definitions behind them do not agree.
  • FACT_TWIN: the same business process counted at two grains, like current subscribers beside every subscription ever.
  • VERSIONED_TWIN: a name reads as a version or leftover of another, like dim_users_v2 beside dim_users, with nothing marking which one is current.

Each finding is a prediction: the agent could confuse these definitions. The eleven findings collapse into eight problems in the chart below, because some pairs trip more than one rule. That chart also carries what happened next, so it is worth reading twice: once now, for what a scan can see before anything runs, and once after the benchmarks below, for what each problem cost.

Does the linter help the agent?#

The eval covered two surfaces. I ran gpt-5-mini on thirty-six business questions, three times each: one hundred and eight graded answers per warehouse. Twenty-nine questions targeted sprawl clusters; seven used unambiguous definitions as a baseline. Thirty-one questions stayed inside the metrics layer. Five forced the agent to leave metrics and write raw SQL. A fixed SQL key graded each answer, and the trace showed which metric or table the agent grounded on.

Wrong selection showed up inside governed metrics. On the metric-layer questions, the agent chose the wrong metric in 24% of answers. Counting construction mistakes too, silent errors reached 30%: wrong number, normal-looking answer, no exception.

The scan predicted where failures would land. The six-name active-user cluster was the worst: the agent grounded on a decoy in ten of fifteen answers. Signups, revenue, and the subscriber twin followed. The unambiguous baseline did not show the same pattern.

Scan vs. agent
Ten problems in one warehouse. Eight clusters were flagged by eleven static findings before the agent ran: three revenue names, six active-user aliases, a signup pair with a hidden staff filter, a value-moments scope trap, a current subscriber count beside a lifetime count, a stock beside a running total, dim_users beside dim_users_v2, and fct_subscriptions beside fct_subscriptions_2026_03. Two were invisible because nobody wrote them down: the undocumented staff flags and stale status column. After fixing the warehouse, wrong metric picks fell to zero.
What the scan caught, what the agent then did, and what changed after the fix. Checkmarks mark problems visible before runtime; the invisible rows show what static analysis cannot read.

For a data team, this is the useful read: the checked rows are cleanup work you can catch before an agent ever runs. Duplicate metric names, hidden scope, fact twins, and versioned tables can become CI findings instead of production mistakes.

The right column is the cost of leaving each row alone. Some flagged rows hurt more than their severity label suggests, but the pattern holds: wrong answers concentrate where the warehouse gives the agent competing valid choices. The two bottom rows show the limit. If the meaning lives only in the rows, a linter cannot see it; those problems need modelling, documentation, and data tests.

Clean the context with data modelling#

The fixes were standard analytics engineering work: dimensional modelling, dbt hygiene, clear metric names, documented columns, explicit filters, and deprecated objects removed instead of left beside the current ones.

Repair catalog
SurfaceBeforeAfterModelling rule
Metric definitionsone governed meaning per business questionmrr, recurring_revenue, monthly_recurring_revenuemrr as the governed metric; old names redirected or deleted.One definition per business metric.
new_users, new_signupsnew_signups with population as a visible filter.Qualifiers belong in dimensions and filters.
active_users, actives, dau, engaged_users, mau, monthly_active_usersactive_users; old names become aliases, not metrics.Aliases are metadata, not new definitions.
value_moments, real_value_moments, total_momentsvalue_moments with the segment rule written once.State the business rule once.
current could mean latest complete month or latest snapshot.Current means the latest monthly snapshot in the tool contract and metric docs.Defaults belong in definitions.
Raw warehouse contextmake fallback SQL safe to usedim_users, dim_users_v2dim_users as the canonical model; old table deprecated, then removed.Versioned models need explicit lifecycle.
is_internal, is_testOne documented exclusion flag with the account rule written in column docs.Columns carry business meaning.
status persisted and stale for ended subscriptions.status derived in the model on every run.Do not persist state you can derive.
fct_subscription_monthsfct_subscription_month_snapshotDeclare the grain in the fact name.
fct_subscriptions, fct_subscriptions_2026_03fct_subscriptions; the dated copy moved out of analytics.Backups are not analytics models.
The fixes are ordinary data modelling: reduce competing metric choices in the semantic layer, and make raw warehouse access less ambiguous when the agent falls back to SQL.

This is the same repair motion I mapped in The AI-Readiness Repair Matrix. These are old data-modelling practices: Kimball fact and dimension tables, clear grain, governed metrics, explicit filters, documented columns, and deprecated objects removed or marked. Agents just make the cost of ignoring that work easier to measure.

The human work still matters. Deleting recurring_revenue means telling the team whose dashboard uses it. Deciding which revenue definition wins can be political because teams are measured on these numbers. The working motion is deprecate, redirect, then delete. A CI gate needs an owner, usually analytics engineering or the data platform team.

Benchmarks#

Then I reran the same questions against the fixed warehouse.

Before and after
Before and after fixing what the linter flagged. Wrong metric picked: 34 percent on the sprawled warehouse, exactly zero after the fix. Silent errors from any cause: 37 percent before, 3 percent after. Same model, same 13 questions, same data; the only change is the definitions.
On the questions the metrics layer governs: wrong metric picked, 24% to zero; silent errors, 0.30 to 0.03. The unambiguous baseline stayed at zero. Same model, same questions, same warehouse.

Wrong selection fell to zero. All ninety-three governed answers used the right metric on the fixed warehouse, for both models.

The choice set changed, not the model. The prompt stayed the same. Once the warehouse had one governed revenue metric, the agent could not choose a decoy revenue metric.

Defaults have to be written down. One construction error remained: when the question said "right now", the small model sometimes used last month's complete snapshot. The fix was simple: no period means the latest snapshot for a stock metric.

I also ran the headline cells on a larger model in the same family, gpt-5.6-terra.

Two models, one warehouse, before and after
gpt-5-minigpt-5.6-terra
trapped questions, 3 repsbefore after fixbefore after fix
silent errors0.300.030.110.00
wrong selection0.240.000.110.00
wrong construction0.060.030.000.00
accuracy0.700.970.891.00
coverage1.001.001.000.98
The warehouse fix took wrong selection to zero for both models. The larger model already made fewer SQL-construction mistakes, but it still needed clean definitions to pick from.

The bigger model made almost no construction errors. It still picked the wrong metric when the warehouse exposed plausible definitions without saying which one was governed. Scale helped SQL construction. It did not recover missing business meaning.

Small printDenominators, model snapshots, and what not to overfit. Read it →

The denominators: thirty-six questions, three repetitions, one hundred and eight graded answers per warehouse per model, of which ninety-three are on concepts the metrics layer governs and fifteen are not. The models are gpt-5-mini and gpt-5.6-terra as served by the OpenAI API in August 2026, with no pinned snapshot, so single answers vary between runs. Across runs, wordings and warehouse revisions, the before-fix selection rate has ranged from roughly a fifth to a half of governed answers; 24% is this protocol's measurement on a warehouse whose metrics carry documentation, and it is not a constant. The after-fix result is the stable one: exactly zero, in every run of every revision. The answer key is mine and I authored the fixtures, a bias I manage with a deterministic oracle and trace-checked grounding but cannot remove. Read 24% and zero as a direction with a clear and repeated signal rather than as constants. Everything runs in a public repository, and further replications land there as they run.

Things I got wrong while proving this#

The ambiguity fooled me too.

The first answer key had mistakes. I applied staff exclusions inconsistently. I left time periods implicit. One sentence I added to help the agent created a new ambiguity. A few late questions also needed repair: one decoy metric could pass the tolerance, one expected answer was zero, and two wordings mixed up customers and contracts.

preflight improved the same way: traces failed in ways that made no sense. It missed mrr beside monthly_recurring_revenue until preflight 0.2.0. It missed _v2 table twins until 0.3.0. It missed subscriber counts at two grains until 0.4.0.

That is the point. Care was not enough. The agent got confused. I got confused. The tool got confused. The useful controls were mechanical: static linting, a fixed answer key, traces, and a CI gate.

Lessons learned#

Give the agent fewer valid choices. A semantic layer helps, but only for the questions it covers. If the agent can fall back to raw SQL, raw tables are still part of its context. Clean both surfaces.

Static checks need written context. A linter can read names, definitions, scope, grain, filters, and obvious leftovers. It cannot read meaning that lives only in rows or in people's heads. That is why it missed the staff flags and stale status. The raw-table questions improved after I modeled and documented those columns, but they did not all become correct. Some questions still asked for a business concept the governed layer did not define.

Govern the concepts people actually ask about. One question asked for active subscription contracts. The semantic layer had customer metrics, not a contract-count metric, so the agent used the closest governed number and answered the wrong question. Cleaning existing definitions cannot fix a missing definition. Coverage gaps need new governed concepts.

Use static checks as the base layer, not the whole system. The scan is not a replacement for golden evals, dbt tests, docs, contracts, or runtime guardrails. It is the cheap layer that catches written ambiguity before the agent sees it. For preflight, or any ambiguity linter, fix HIGH findings first and let LOW findings inform. If a team decides a finding is acceptable, overrule it in the pull request and move on.

Bigger models do not replace governed context. The larger model nearly erased construction mistakes, then still picked the wrong revenue metric when the warehouse exposed plausible choices without saying which one was governed. Scale helps the agent use context. It does not create company-specific meaning that is absent or contradictory.

Old modelling practices are the agent-readiness practices. Clean fact and dimension tables, one governed metric per concept, explicit grain, visible filters, documented columns, deprecation windows, and backups kept out of analytics. None of this is new. Agents just make the cost of ignoring it easier to measure.

OpenAI's internal data-agent write-up points at the same problem from the other end. At their scale, they reconstruct meaning industrially from pipeline code, annotations, ownership, usage, and lineage so the agent can select well at answer time. Most teams do not need that machinery. At a few hundred models, prevention is still available: clean names, governed definitions, documented columns, old objects removed, and a preflight scan in CI.

Most warehouses have a real_something: a metric people know to use only because someone once explained it to them. That is already a weak contract for humans. For an agent, it is not a contract at all. In this experiment, pairs like that produced wrong selections in 24% of the answers where a governed metric existed to be chosen. The fix was simple: remove the ambiguity, then block it from coming back.

Read next

Want to build a clearer decision system?

Tell us where the numbers feel murky and we'll show you what a trustworthy decision system looks like for your team.