Request a callbackBook a call
← All posts

Build an AI Data Analyst Agent: Schema Grounding, SQL Sandboxing and the Rails That Stop a Wrong Number (2026)

TL;DR
  • One answered question costs about 5 cents against roughly $25 of analyst time, and one unguarded query can cost $25 on its own. A byte-scanned cap is not a cost optimisation in this product. It is the difference between a five-cent answer and a five-hundred-times overrun on a single typo.
  • Text-to-SQL benchmarks do not transfer to your warehouse. Models score around 73% execution accuracy on BIRD but peak near 24% on Spider 2.0, which is built from real enterprise databases with over a thousand columns. Any design that assumes the model will get the join right is the wrong design.
  • The rails are the product: a curated semantic layer instead of a raw schema dump, an AST allowlist instead of a prompt saying please do not DROP, a read-only role with a dry run and a byte cap, and a provenance card showing the SQL and filters beside every number.
The six rails between a question and a number in a board deck
1 · Semantic grounding

Retrieve a curated subset of certified tables, columns and metric definitions. Never dump a thousand-column schema into a prompt and hope.

$0.0002 · fails: wrong grain
2 · Static validation

Parse the generated SQL into an AST. Single statement, SELECT only, tables on the allowlist, no functions that touch the filesystem or network.

$0.00 · fails: nothing, if built
3 · Sandboxed execution

A read-only role on a dedicated warehouse, a statement timeout, a row limit and a hard byte-scanned cap that rejects the query before it costs anything.

caps a $25 query at $0.06
4 · Result verification

Row count, null share, magnitude against historical range, and a check that the filters applied match the filters the question asked for.

$0.004 · fails: plausible wrong
5 · Deterministic charts

A chart specification derived from the result shape and column types, rendered by your chart library. The model never draws an image or invents an axis.

$0.00 · fails: mislabelled axis
6 · Provenance card

The SQL, the filters, the row count, the freshness of the underlying tables and the run id, shown with every number. Not in a tooltip.

the reason anyone trusts it
Only the first rail involves retrieval and only the fourth involves a model. The other four are ordinary deterministic engineering, and together they are what separates a data analyst agent from a very fast way to put an unverified number in front of a board.

What is an AI data analyst agent, architecturally?

A semantic grounding layer, a SQL generation step, a static validator, a sandboxed executor, a result verifier and a deterministic chart renderer, with a provenance card attached to every output. The model writes SQL. Everything else exists to make sure the SQL it wrote is safe to run and the number it returned is safe to quote.

The framing that gets this wrong is calling it chat with your data. That implies the hard part is the conversation, when the hard part is that a warehouse has a thousand columns, four tables that all look like they contain revenue, three definitions of an active user, and a fact table whose grain is not what its name suggests. A model asked to write SQL against that produces a query that runs, returns a number, and is wrong in a way nobody catches until a quarterly review.

The benchmark evidence is unusually clear and worth putting in front of anyone who thinks this is solved. On BIRD, execution accuracy sits around 73% under a strict exact-match standard. On Spider 2.0 (632 tasks built from real enterprise databases, many with over a thousand columns, on BigQuery and Snowflake) GPT-4o solved 10.1% against 86.6% on the original Spider, o1-preview reached 17.1%, and the peak reported figures were around 23.8% on Spider 2.0-snow and 23.4% on Spider 2.0-lite. A system architected on the assumption that the model gets the join right is architected against the evidence.

That is not an argument against building this. It is an argument for where the engineering goes. Every point of accuracy in a real deployment comes from narrowing what the model has to figure out: a curated semantic layer instead of a raw schema, certified metric definitions instead of ad-hoc aggregation, a small set of approved joins instead of the full graph, and a refusal path for questions outside the covered surface. The model then writes simple SQL over a well-described subset, a task it is genuinely good at.

This is a reference design: how I would build it and what the arithmetic says it costs. It draws on architecting AccioMatrix, an event-driven platform orchestrating multiple LLMs, agents and MCP tooling for 20+ enterprise clients, and on cutting a $200K-plus annual cloud bill by more than 70%, which is where my instinct for hard spend caps on anything that queries at scale comes from. I have not shipped a natural-language analytics product in production, and every figure below is modelled with its inputs printed.

ComponentWhat it doesImplementationCost per questionPrimary failure modeSkip in v1?
Question routerCertified metric lookup versus novel SQL versus refusalDeterministic match against the metric catalogue first, model second~$0.0000Routes a certified-metric question to free-form SQL and gets a different number than the dashboardNo
Semantic groundingRetrieve the relevant slice of a curated model: tables, columns, metrics, approved joinsHybrid search over documented entities, not a schema dump$0.0002Wrong grain selected; the query double-counts and returns a plausible larger numberNo — this is the accuracy lever
SQL generationOne typed call producing SQL plus a stated assumption listSonnet-class, ~9k in with a 5k cached prefix / 500 out$0.0140Confidently invents a column that does not exist; caught by the validator, not the modelNo
Static validationAST parse: single statement, SELECT only, allowlisted tables, banned functionsA real SQL parser. Never a regex, never a prompt instruction$0.0000Not built, so the only thing between a model and your warehouse is a polite requestAbsolutely not
Sandboxed executionRead-only role, statement timeout, row limit, byte-scanned cap, dry run firstDedicated warehouse or virtual warehouse with its own quota$0.0073 (1.2 GiB scan)An unbounded scan: 4 TiB on BigQuery on-demand is $25 for one questionNo
Repair loopOne bounded retry with the error and schema quoted backMax one retry, mutated context, then refuse$0.0064 amortisedUnbounded retries burn tokens and warehouse spend on a question that cannot be answeredNo — but cap it at one
Result verificationRow count, null share, magnitude versus historical range, filter-intent matchDeterministic checks plus one cheap model call for intent alignment$0.0040Empty result presented as zero — the most dangerous non-error in the productNo
Chart specificationChart type, axes, units and formatting derived from result shape and column typesDeterministic rules; your chart library renders it$0.0000Model-drawn chart with an invented axis label or a truncated y-axisNo
Narrative and provenanceA short reading of the result plus the SQL, filters, row count and data freshnessSonnet-class, ~6k in / 600 out, provenance assembled deterministically$0.0180Narrative asserts a cause the query cannot supportNarrative: yes. Provenance: never

What does the full architecture look like?

Twelve components in four columns: the surface and the router, grounding and generation, validation and sandboxed execution, and the warehouse plus rendering and provenance. The unusual feature: two independent gates sit between the model and the data, a static one before execution and a semantic one after it, because a query can be perfectly safe and completely wrong.

Start with the router, the cheapest accuracy win in the system. A large share of real questions ask for a metric that is already defined: monthly recurring revenue, weekly active users, gross margin by region. Answer those by parameterising a certified query, not by generating SQL from scratch. If the agent generates its own revenue calculation, it eventually produces a number that differs from the dashboard by 3%, and every subsequent conversation in that organisation is about which number is right rather than what the number means.

Semantic grounding is where the accuracy comes from. Do not put the schema in the prompt. Put a curated model in the prompt: the twenty certified tables, each with a described grain, the columns safe to filter on with their real meanings, the approved joins with their cardinality, the metrics with their canonical SQL, and the known traps: the deprecated table that still has rows, the column whose name says amount but whose unit is minor currency. Retrieve the relevant slice per question and cache the stable part of the prefix so it bills at roughly a tenth of input price.

The validation and execution column is deliberately paranoid, and it should be. The static validator parses the generated SQL into a syntax tree and rejects anything that is not a single SELECT over allowlisted objects, using a real parser rather than a regex and certainly not a line in the system prompt asking the model not to write DDL. Then the executor runs a dry run for an estimated byte scan, enforces a hard cap, applies a statement timeout and a row limit, and connects as a role with no write privileges anywhere. Defence in depth, because the model is untrusted input by construction: if a user can type a question, a user can attempt a prompt injection.

Everything after execution is about trust rather than correctness. The result verifier applies deterministic sanity checks. The chart specification is derived from column types by rules, so the model never draws an axis. And the provenance card (the SQL, the filters applied, the row count, the freshness of the underlying tables, the run id) appears next to the number rather than behind a tooltip. That last decision makes the output quotable, because it lets a reader check the thing instead of trusting it. The general layering argument is in the AI product architecture reference design.

The system
Data analyst agent: full reference architecturecertified matchnovel questionrefuse: metric undefinedcurated slicecandidate SQLpasses AST checksone repair retrydry run, then executesaneflagged: show, do not chart
Chat / BI surfacequestion in · provenance card out
Question routercertified metric · novel SQL · refuse
Certified metric cataloguecanonical SQL, owner, grain, freshness
Semantic groundingcurated tables, columns, joins, traps
SQL generationSQL + stated assumptions, one turn
Run + query journalSQL, bytes, cost, verdict, human edits
Static validatorAST: single SELECT, allowlist, banned fns
Sandboxed executorread-only role · dry run · byte cap · timeout
Result verifierrows, nulls, magnitude, filter-intent match
WarehouseBigQuery / Snowflake · $6.25 per TiB scanned
Chart spec + narrativedeterministic spec · model writes prose only
Provenance cardSQL · filters · rows · freshness · run id
Three dashed edges, and all three are refusals or fallbacks. That ratio is correct for this product. A data analyst agent that never says the metric you are asking about is not defined, here is the closest certified one is a data analyst agent that is guessing, and a guess rendered as a bar chart is indistinguishable from an answer.
Schema dump versus curated semantic layer
Dump the schema into the prompt
Fast to build, and it is why enterprise text-to-SQL benchmarks are so brutal
  • A real warehouse table can carry over a thousand columns; Spider 2.0 was built from exactly these databases
  • The model must infer grain, and grain errors double-count silently into a larger, plausible number
  • Four tables look like revenue and nothing in the schema says which one finance uses
  • Deprecated tables still have rows and no schema annotation says do not use this
  • Prompt grows without bound, cache prefix churns, and cost rises with schema size rather than question difficulty
pick
Retrieve a curated slice
Two to four weeks of unglamorous modelling work, and it is where the accuracy comes from
  • Twenty certified tables with a written grain, not four hundred with none
  • Metrics carry canonical SQL and a named owner, so the agent agrees with the dashboard by construction
  • Approved joins with cardinality, which removes the single largest class of wrong answers
  • Known traps documented in the model: minor-unit columns, deprecated tables, timezone conventions
  • Stable prefix caches at roughly a tenth of input price, so cost tracks question volume rather than schema size
This is the decision that determines whether the product works. The published benchmark gap (around 73% execution accuracy on BIRD against a peak near 24% on Spider 2.0, whose databases are real enterprise warehouses) is largely a gap between a tidy schema and a real one. A curated semantic layer is how you give the model the tidy version of your own warehouse.

What happens when someone asks a question, end to end?

Route, ground, generate SQL, parse it into a syntax tree and reject anything unsafe, dry-run for a byte estimate, execute under a read-only role with a hard cap, verify the result, derive a chart specification, write a short narrative and attach a provenance card. About eight seconds, three model calls, roughly five cents.

The step most implementations omit is the dry run, and it is free on BigQuery. A dry run returns the estimated bytes the query will process without executing it, so you can reject a query that would scan four terabytes before it costs anything. Enforcing maximum_bytes_billed does the same thing server-side, failing the query without incurring a charge. Both belong in the executor. A single unbounded SELECT against a large partitioned table is a $25 question, and the failure is not a model failure, it is a missing WHERE clause on a date partition.

The repair loop is capped at one retry and the retry must mutate the context. Sending the same request again after a SQL error reproduces the same error at low temperature and charges you twice. The retry has to append the exact database error, the offending identifier and the relevant slice of the curated schema, then take a fresh turn. If the second attempt fails, refuse and say why. An unbounded repair loop against a warehouse is the one place in this product where a bug can generate real money, because each attempt burns both tokens and scanned bytes.

Result verification runs before anything is rendered and it is mostly arithmetic. Zero rows returned is not zero. It is an unanswered question, and presenting an empty result as a value of zero is the single most dangerous non-error in the product. A result whose magnitude is far outside the historical range for that metric is flagged rather than charted. A result whose applied filters do not match the filters the question asked for is flagged. Only the last needs a model, and it runs on a cheap one.

Finally, the chart is a specification, not a picture. Given a result with one date column and one numeric column, a line chart with the date on x, formatted in the metric's unit, is a deterministic rule. Letting a model choose axes and labels introduces a class of error (a truncated y-axis, an invented unit, a mislabelled series) that is invisible to every check you have and highly visible in a board deck.

The answer path
One question, end to end, with tokens, bytes and costUserRouterGroundingGeneratorValidatorSandboxWarehouseVerifier
what was net revenue by region last quarter?
certified metric net_revenue matched, region not parameterised
$0.0000 · deterministic
novel: certified metric + new dimension
retrieve 6 tables, 41 columns, 3 joins, 2 traps
$0.0002
curated slice + canonical net_revenue SQL
generate: 9.0k in (5.0k cached) / 500 out
$0.0140 · Sonnet 5
candidate SQL + stated assumptions
AST: 1 statement, SELECT only, 3 allowlisted tables
$0.0000 · real parser
approved for execution
dry run: estimate 1.2 GiB
free on BigQuery
execute · read-only role · cap 10 GiB · 30s timeout
$0.0073 at $6.25/TiB
4 rows, 0 nulls, values within 1.4x of trailing range
filter-intent check: 3.0k in / 200 out
$0.0040 · Haiku 4.5
chart spec + narrative + provenance card
$0.0180 narrative · Sonnet 5
Total modelled cost: $0.0499 including an amortised repair retry. The two steps that prevent the most expensive failures, the AST validation and the dry run, cost exactly nothing. The single largest line is the narrative at $0.018, worth questioning: many teams would be better served shipping the number, the chart and the provenance card with no prose at all.
sql/guard.ts
import { Parser } from "node-sql-parser";
import { BigQuery } from "@google-cloud/bigquery";

const parser = new Parser();

const BANNED_FUNCTIONS = new Set([
  "external_query", "net.host", "session_user", "current_user",
]);

export type Verdict =
  | { ok: true; tables: string[] }
  | { ok: false; reason: string; detail: string };

export function validateSql(sql: string, allowlist: Set<string>): Verdict {
  let ast;
  try {
    ast = parser.astify(sql, { database: "bigquery" });
  } catch (e) {
    return { ok: false, reason: "unparseable", detail: String(e) };
  }

  const statements = Array.isArray(ast) ? ast : [ast];
  if (statements.length !== 1) {
    return { ok: false, reason: "multiple_statements", detail: String(statements.length) };
  }
  if (statements[0].type !== "select") {
    return { ok: false, reason: "not_a_select", detail: statements[0].type };
  }

  // parser.tableList returns entries shaped "select::db::table"
  const { tableList } = parser.parse(sql, { database: "bigquery" });
  const tables = tableList.map((t: string) => t.split("::").slice(1).join("."));
  for (const t of tables) {
    if (!allowlist.has(t)) {
      return { ok: false, reason: "table_not_allowlisted", detail: t };
    }
  }

  const lowered = sql.toLowerCase();
  for (const fn of BANNED_FUNCTIONS) {
    if (lowered.includes(fn)) {
      return { ok: false, reason: "banned_function", detail: fn };
    }
  }

  return { ok: true, tables };
}

const MAX_BYTES = 10 * 1024 ** 3;   // 10 GiB -> at most ~$0.061 at $6.25/TiB
const TIMEOUT_MS = 30_000;
const MAX_ROWS = 5_000;

export async function execute(sql: string, allowlist: Set<string>) {
  const verdict = validateSql(sql, allowlist);
  if (!verdict.ok) return { status: "rejected" as const, verdict };

  // read-only service account: no write grants on any dataset
  const bq = new BigQuery({ projectId: process.env.ANALYTICS_PROJECT });

  // free: returns the byte estimate without running the query
  const [dry] = await bq.createQueryJob({ query: sql, dryRun: true });
  const estimated = Number(dry.metadata.statistics.totalBytesProcessed);
  if (estimated > MAX_BYTES) {
    return {
      status: "rejected" as const,
      verdict: { ok: false as const, reason: "byte_cap_exceeded", detail: String(estimated) },
    };
  }

  const [rows] = await bq.query({
    query: sql,
    maximumBytesBilled: String(MAX_BYTES), // warehouse-side belt to the dry-run braces
    jobTimeoutMs: TIMEOUT_MS,
    maxResults: MAX_ROWS,
  });

  return { status: "ok" as const, rows, estimatedBytes: estimated };
}
The two gates between a generated string and your warehouse. validateSql parses into an AST and enforces structure, never a regex over the SQL text, because comments, string literals and CTE aliases will defeat any pattern you write. execute then dry-runs for a byte estimate and sets maximum_bytes_billed so the warehouse itself rejects an over-limit query without charging for it. The connection uses a role with no write grants anywhere, the rail that holds when the other two have bugs.

How do you stop it hallucinating a number into a board deck?

By making the number checkable rather than trusting it. Certified metrics carry canonical SQL so the agent agrees with the dashboard by construction. Deterministic verification catches empty results, implausible magnitudes and mismatched filters. And a provenance card puts the SQL, the filters, the row count and the data freshness beside the number so a reader can audit it in five seconds.

Empty results deserve special handling because they are the most dangerous output in the product. A query that returns no rows because the region name was spelled differently in the warehouse looks, downstream, exactly like a query that returns no rows because the value is genuinely zero. Render those two differently and force the distinction: an empty result is a failed question, and the correct output is no rows matched these filters, here are the distinct values that do exist in that column. That single behaviour prevents more bad decisions than any accuracy improvement you can buy.

Magnitude checking is the second cheap win. Store, for each certified metric, the trailing range of values it has taken at the relevant grain. A result well outside that range is not necessarily wrong (quarters do end, campaigns do land) but it is worth flagging, and the flag costs nothing. Present it as this is 4.1x the trailing twelve-week range for this metric rather than blocking it, because the flag is information for the reader, not a verdict.

The third rail is refusal, and it needs teeth. When the question asks for a metric that is not defined, say so and offer the closest certified metric with its definition, rather than construct a reasonable-looking aggregation. Organisations that let an agent invent metric definitions get a proliferation of subtly different revenue numbers, and the cost is not measured in dollars per query. It is measured in the meetings spent reconciling figures.

None of this requires a smarter model. It requires a metric catalogue, four deterministic checks and a UI decision about where provenance lives. Notice the shape: the same discipline as citation spans in a support answer and quoted spans in a research brief. In every one of these products the design move is the same: make the output carry the evidence that lets someone else check it, and delete anything that cannot.

semantic/net_revenue.yml
metric: net_revenue
label: "Net revenue"
owner: "finance-analytics@example.com"
certified: true
certified_at: "2026-08-04"

grain: "one row per order line, deduplicated on order_line_id"
unit: "USD, minor units (cents) in the column, divided by 100 in the metric"
timezone: "UTC. Finance reports in UTC; product analytics uses local. Do not mix."

sql: |
  SELECT SUM(gross_amount_minor - discount_minor - refund_minor) / 100.0
  FROM analytics.fct_order_line
  WHERE order_status IN ('completed', 'partially_refunded')

dimensions:
  - name: region
    column: analytics.dim_customer.region_code
    join: "fct_order_line.customer_key = dim_customer.customer_key (many-to-one)"
  - name: product_line
    column: analytics.dim_product.product_line
    join: "fct_order_line.product_key = dim_product.product_key (many-to-one)"

time_column: analytics.fct_order_line.completed_at
partition_column: analytics.fct_order_line.completed_date   # ALWAYS filter this

traps:
  - "analytics.revenue_daily is DEPRECATED but still receives rows from a legacy job.
     It excludes refunds. Never use it for a revenue question."
  - "gross_amount_minor is in MINOR units. A query that forgets /100 returns a number
     that is 100x too large and still looks like money."
  - "Orders in status 'pending_capture' are not revenue. They are in the fact table."
  - "Querying without a completed_date filter scans the full partition set:
     roughly 4 TiB, about $25 on BigQuery on-demand, for one question."

plausible_range:
  grain: "weekly, all regions"
  p05: 780000
  p95: 1420000
  note: "Trailing 52 weeks. Flag, do not block, results outside this band."

refuse_if:
  - "The question asks for 'revenue' without qualifying gross or net AND the answer
     would differ by more than 2%. Ask which one instead of guessing."
One certified metric. The parts that matter are not the SQL: they are owner, grain, the explicit unit, the documented traps and the plausible range. The traps section is what stops a model discovering the deprecated table and quietly using it, and the plausible_range feeds the magnitude check. This file is the artefact the whole product depends on, and writing forty of them is two to four weeks of unglamorous work that no model can do for you.
Checklist
The eight rails, in the order they pay off
  • Certified metrics with canonical SQL and a named ownerThe agent agrees with the dashboard by construction. Removes the which-number-is-right meeting entirely.
  • AST validation: single statement, SELECT only, allowlisted tablesA real parser. A regex over SQL text is defeated by comments, string literals and CTE aliases.
  • Read-only role on a dedicated warehouse or virtual warehouseThe rail that holds when the other two have bugs. No write grants anywhere, ever.
  • Dry run plus a hard byte-scanned capFree on BigQuery, and it converts a $25 runaway into a rejection that costs nothing.
  • Empty results rendered as unanswered, never as zeroThe most dangerous non-error in the product. Show the distinct values that do exist instead.
  • Magnitude check against a stored plausible rangeFlag, do not block. 4.1x the trailing range is information for the reader, not a verdict.
  • Provenance card beside the number, not in a tooltipSQL, filters, row count, table freshness, run id. This is what makes a number quotable.
  • Refusal when the metric is undefinedOffer the closest certified metric with its definition. Hardest rail to ship because it feels like a worse demo.
The last item is the one that gets negotiated away in a stakeholder review, because a system that refuses looks less capable than one that always answers. It is also the only rail that prevents an organisation accumulating six slightly different definitions of revenue, which is a cost that never appears on any infrastructure bill.
Certified-metric coverage against accuracy and cost per question
10276512500%20%40%60%80%Modelled accuracy (%) · cost per question (cents)Share of questions covered by certified metrics
Modelled answer accuracy (%)Cost per question (cents)
Both curves improve together, which is unusual and worth exploiting: a parameterised certified metric skips SQL generation, skips most of the repair loop and scans a partition-filtered slice, so it is cheaper as well as more likely to be right. These are modelled figures illustrating the mechanism, not measurements. The anchor points are the published benchmarks, roughly 73% execution accuracy on BIRD against a peak near 24% on Spider 2.0's real enterprise databases, the gap a semantic layer exists to close.

What does one question cost?

About five cents on the model here, against roughly $25 of analyst time for the same ad-hoc question. The variance is the real story: the same architecture without a byte cap can produce a $25 warehouse charge on a single query, five hundred times the intended cost, and it arrives with no error and no alert.

The composition is unusual for an AI product. SQL generation is $0.0140, the narrative is $0.0180, verification is $0.0040, grounding retrieval is $0.0002, the amortised repair retry is $0.0064, and warehouse execution at 1.2 GiB scanned is $0.0073 at BigQuery's on-demand rate of $6.25 per TiB. Two thirds of the bill is models and a third is the warehouse. But the warehouse third is the only line with unbounded upside, because tokens are capped by your context window and scanned bytes are capped by nothing except the cap you set.

That asymmetry should drive the design. A missing partition filter on a large fact table is not exotic; it is what happens when a model writes a WHERE clause on completed_at instead of completed_date. Modelled against a 4 TiB partition set, that one query is $25. Enforce the cap in two places, a dry-run check in your executor and maximum_bytes_billed on the job (which BigQuery documents as rejecting the query before it incurs charge), and the same mistake becomes a rejection costing nothing.

Against the human baseline the case is straightforward. An ad-hoc question routed to a data analyst takes roughly 25 minutes including context switching and clarification, which at $60/hour fully loaded is about $25. The agent answers in eight seconds for five cents. But the honest comparison includes two things the arithmetic omits: the analyst is usually right and the agent sometimes is not, and the analyst's real cost is the two-day queue delay rather than the twenty-five minutes. The value here is latency and volume, not headcount.

The narrative line deserves scrutiny. At $0.0180 it is the single largest model cost, and the component most likely to assert something the query cannot support: a cause, a trend, a comparison to a period that was not queried. Many teams would be better served shipping the number, the chart and the provenance card with no prose at all, which removes 36% of the cost and an entire class of error at once. The routing and caching machinery that makes the rest cheap is covered in LLM routing and caching, cost per request.

Line itemModel / rateUnitsCostShareNote
Question routingDeterministic catalogue match1 lookup$0.000000%A certified-metric hit skips generation entirely and is both cheaper and more accurate
Semantic groundingHybrid retrieval over the curated model6 tables, 41 columns, 3 joins$0.000200.4%Never a schema dump. Retrieval cost is flat in warehouse size
SQL generationSonnet 5 · $2 / $10 per 1M9,000 in (5,000 cached) / 500 out$0.0140028%Cached prefix is the curated model plus few-shot; it bills at ~10% of input
Static validationAST parse1 statement$0.000000%Free, and it prevents the failure class with the worst blast radius
Dry runBigQuery dry run1 estimate$0.000000%Documented as free; returns bytes processed without executing
Warehouse executionBigQuery on-demand · $6.25 per TiB1.2 GiB scanned$0.0073315%First 1 TiB per month is free; Snowflake bills $2-$4 per credit instead
Repair retrySonnet 5 + warehouse, on ~30% of questionsamortised$0.0064013%Capped at one. The retry must quote the error back or it reproduces it
Result verificationHaiku 4.5 · $1 / $5 per 1M3,000 in / 200 out$0.004008%Deterministic checks are free; only filter-intent alignment is a model call
Chart specificationDeterministic rules1 spec$0.000000%The model never draws a chart or chooses an axis
NarrativeSonnet 56,000 in / 600 out$0.0180036%The largest line and the most questionable. Consider shipping without prose
Journal + provenancePostgres, amortised~6 rows$0.000200.4%SQL, bytes, cost, verdict, and any human correction
Total per question$0.04993100%vs ~$25 of analyst time at $60/hr and 25 minutes
Unguarded runawaySame query, no partition filter, no cap4 TiB scanned$25.00000500xOne missing WHERE clause. This is why the cap exists in two places
Where five cents goes
$ per answered question, novel-SQL pathlower is better
Narrative generation (Sonnet 5)36%. The line most worth deleting$0.0180
SQL generation (Sonnet 5)28%$0.0140
Warehouse execution (1.2 GiB at $6.25/TiB)15%. The only line with unbounded upside$0.0073
Repair retry (amortised over 30%)13%$0.0064
Result verification (Haiku 4.5)8%$0.0040
Grounding retrieval + journal<1%$0.0004
Validation, dry run, chart specfree, and they prevent the $25 failure$0.0000
Total$0.0499
Three components cost nothing and prevent the most expensive failure in the system. The most expensive component writes prose the query cannot always support. If you are looking for a first optimisation, it is not a cheaper model. It is asking whether the narrative should exist at all.
The four numbers that frame this build
$0.050
modelled cost of one answered question, including an amortised repair retry
$25.00
modelled cost of one unguarded query scanning a 4 TiB partition set at $6.25/TiB
500x
~24%
peak reported execution accuracy on Spider 2.0, built from real enterprise warehouses
~73%
execution accuracy on BIRD under its strict exact-match standard
The two right-hand figures are published benchmark results, not properties of this design, and they are why the semantic layer exists. The gap between them is roughly the gap between a well-formed academic schema and a real warehouse with a thousand columns per table, exactly the gap a curated model closes for your own data.

What breaks in production, and how would you know?

Ten things, and almost all of them return a number. That is the defining property of this product: unlike a support agent, which can fail visibly, a data analyst agent fails by producing a well-formatted, correctly charted, confidently narrated figure that is wrong. No error rate moves and no dashboard turns red.

The two worst are grain errors and silent filter mismatches. A join at the wrong cardinality double-counts and returns a larger number that looks like good news, the direction of error least likely to be questioned. A filter that does not match the question (last quarter read as the last 90 days rather than the fiscal quarter) returns a number close enough to be plausible and different enough to matter. Both are caught by the same discipline: document the grain in the semantic layer, restate the applied filters in the provenance card, and make the restatement prominent enough that a reader notices when it is wrong.

The cost failure is the one with a number attached. An unbounded scan on BigQuery on-demand at $6.25 per TiB turns a five-cent question into a $25 one, and an agent looping repair attempts against a large table can do that repeatedly. Three defences: cap the repair loop at one attempt, enforce maximum_bytes_billed so the warehouse rejects the job before charging, and set a per-user daily quota so a single enthusiastic exploration cannot consume a month of budget. The quota is the one teams skip and the one that turns an incident into an inconvenience.

Then the failure that is organisational rather than technical. If the agent computes revenue its own way and finance computes it another, the organisation now has two numbers, and the cost is measured in meetings rather than dollars. This is why certified metrics with named owners are the first rail rather than a later refinement: the agent should be structurally incapable of disagreeing with the dashboard on a metric that has a definition.

Four signals catch most of the rest: the share of questions answered from certified metrics, the rejection rate at the static validator broken out by reason, bytes scanned per question at p95 and p99, and the human correction rate on answers that were charted. The last requires a thumbs-down with a reason, which costs one button and is the only direct measure of whether the numbers are right. Everything else tells you the system is running, not that it is correct.

Failure modeWhat the user seesWhere to fix itDetection signalCost of getting it wrong
Join at the wrong grain double-countsA larger, plausible number that nobody questionsSemantic layer: document grain and approved joins with cardinality; restate the join path in provenanceResult magnitude versus stored plausible range; correction rate by metricA wrong number in a board deck. The error direction least likely to be challenged
Empty result presented as zeroA confident zero for a question that did not matchVerifier: render empty as unanswered and list the distinct values that do existZero-row rate by question class; zero-valued answers that were chartedA decision made on the belief that something is not happening when it is
Filter interpreted differently than askedLast 90 days when the question meant the fiscal quarterProvenance card: restate applied filters prominently; verifier checks intent alignmentFilter-intent mismatch rate from the cheap verifierClose enough to be plausible, different enough to matter
Unbounded scan on a large tableA slow answer, then a warehouse invoice nobody expectedExecutor: dry run, maximum_bytes_billed, statement timeout, per-user daily quotaBytes scanned p95 and p99; rejections by byte cap; daily spend per user$25 for one question at $6.25 per TiB over a 4 TiB partition set
Repair loop retries against the warehouseNothing, then a large billRuntime: cap at one retry, mutate context with the error, then refuseRetries per question distribution; spend per question outliersMultiplies the runaway above by the retry count
Agent invents its own metric definitionA revenue figure 3% off the dashboardRouter: certified metric lookup first; refuse to construct undefined metricsShare of answers from certified metrics; disagreement with dashboard valuesMeasured in meetings, not dollars, and it compounds
Deprecated table still returns rowsA clean answer computed from stale, incomplete dataSemantic layer: traps section naming the table and why not to use it; drop it from the allowlistAllowlist coverage audit; queries touching deprecated objectsSystemic and invisible; the query is valid and the data is not
Prompt injection through a questionA query attempting something outside the intended scopeStatic validator plus read-only role. Defence in depth, never a prompt instructionValidator rejections by reason; anomalous table referencesThe read-only role is what holds when the other rails have bugs
Narrative asserts a cause the query cannot supportRevenue rose because of the pricing changeNarrative prompt constrained to describing the result; causal language removed by ruleCausal-language detection rate in narratives; human corrections on proseA causal claim quoted onward as a finding, with a chart attached
Stale data presented as currentYesterday's number labelled as todayProvenance: table freshness timestamp shown with every answer, sourced from the warehouseMax table lag at answer time; answers served against tables past their SLAA decision made on data that had not landed yet
Unlike almost any other AI product, this one does not fail visibly. It fails by returning a well-formatted, correctly charted, confidently narrated number that is wrong, which is why every rail in the design is about making the number checkable rather than making the model smarter.The design premise of a data analyst agent

What does it take to build, and what should you skip?

About eight engineer-weeks for a two-person team plus two to four weeks of an analytics engineer's time on the semantic layer, which runs in parallel and is the real critical path. Skip multi-agent, skip fine-tuning, skip write access, skip open-ended exploration in v1, and skip any promise of full warehouse coverage.

Week one is the journal and the sandbox: a read-only role, a dedicated warehouse or virtual warehouse with its own quota, the dry-run path, the byte cap and the query journal recording SQL, bytes, cost and verdict for every attempt. Building the cage before the animal is the correct order and is nearly always reversed, because generating SQL is the fun part and it demos in an afternoon.

Weeks two and three are the static validator and the semantic-layer plumbing: AST parsing, the allowlist, retrieval over certified entities and cache-friendly prompt assembly. In parallel, an analytics engineer writes the metric definitions, and that work (twenty to forty metrics with grain, units, joins, traps and plausible ranges) is what determines whether the product works. No model substitutes for it, and no engineer without domain knowledge can write it.

Week four is generation with structured output and stated assumptions. Week five is the verification suite: zero-row handling, magnitude checks, filter-intent alignment and the provenance assembly. Week six is charts, as deterministic specifications driven by result shape and column types. Weeks seven and eight are evaluation: a golden set of a few hundred real questions with known-correct answers, the only way to know your accuracy on your warehouse, and worth more than any benchmark number you will read.

On skipping: this is not a multi-agent problem. A planner, a SQL writer and a critic looks appealing and mostly multiplies tokens; the reasoning is in multi-agent versus single agent. Do not give it write access, ever: no CREATE TABLE for scratch work, no materialised results, no temporary tables in a shared dataset. And do not promise coverage of the whole warehouse. Ship twenty certified metrics and four approved dimensions, be visibly excellent inside that boundary and visibly honest outside it, and expand from evidence. For the general scoping discipline, building an MVP in days with AI covers it, and the ongoing architecture work is what AI product development is for.

Eight engineer-weeks plus a parallel semantic-layer track
  1. Week 1
    Sandbox and journal first

    Read-only role, dedicated warehouse with its own quota, dry-run path, byte cap, statement timeout, per-user daily limit, and a query journal recording SQL, bytes, cost and verdict for every attempt including rejections.

  2. Weeks 2‑3
    Validator and grounding

    AST parsing with a real SQL parser, the table allowlist, retrieval over certified entities, and cache-friendly prompt assembly so the curated model bills at a tenth of input price.

  3. Weeks 2‑5 (parallel)
    The semantic layer: the actual critical path

    An analytics engineer writes 20-40 metric definitions with grain, units, timezone, approved joins, documented traps and plausible ranges. No model can do this and no engineer without domain knowledge can either.

  4. Week 4
    SQL generation

    One typed call producing SQL plus a stated assumption list, structured output enforcement, and a repair loop capped at exactly one context-mutating retry.

  5. Week 5
    Verification and provenance

    Zero-row handling, magnitude checks against plausible ranges, filter-intent alignment on a cheap model, and the provenance card assembled deterministically from the journal.

  6. Weeks 6‑8
    Charts, evals, rollout

    Deterministic chart specifications from result shape. Then a golden set of a few hundred real questions with known-correct answers, the only measure of accuracy on your warehouse that means anything.

The parallel track is the one that decides the outcome and the one that cannot be compressed by adding engineers, because it needs someone who knows which of your four revenue tables finance actually uses. Start it in week one, staff it with a person who has that knowledge, and treat the engineering schedule as constrained by it.

Building an AI data analyst agent: common questions

How accurate is natural language to SQL in 2026?

It depends entirely on the schema. On BIRD, execution accuracy sits around 73% under a strict exact-match standard. On Spider 2.0 (632 tasks built from real enterprise databases on BigQuery and Snowflake, many tables carrying over a thousand columns) GPT-4o solved 10.1% against 86.6% on the original Spider, o1-preview reached 17.1%, and peak reported figures were around 23.8%. The practical consequence: accuracy in your deployment is a function of how much you narrow the problem with a curated semantic layer, not of which model you pick.

How much does one natural-language question cost?

About 5 cents on the model in this post: $0.014 for SQL generation, $0.018 for the narrative, $0.0073 for a 1.2 GiB warehouse scan at BigQuery on-demand rates of $6.25 per TiB, $0.004 for verification and $0.0064 for an amortised repair retry. The number that matters more is the worst case: an unguarded query scanning a 4 TiB partition set is about $25, five hundred times the intended cost, which is why the byte cap belongs in the executor and on the job.

How do you stop the agent from running a destructive or runaway query?

Three independent rails. Parse the generated SQL into a syntax tree and reject anything that is not a single SELECT over allowlisted tables, using a real parser, because a regex is defeated by comments and string literals. Connect as a role with no write grants on any dataset, which holds when the validator has a bug. And run a dry run for a byte estimate plus maximum_bytes_billed on the job, which BigQuery documents as failing an over-limit query without incurring a charge. Add a per-user daily quota so one exploration cannot consume a month of budget.

Should the agent see the raw database schema?

No. Retrieve a curated slice of a documented semantic model: certified tables with a written grain, columns with real meanings and units, approved joins with cardinality, canonical metric SQL, and a traps section naming the deprecated table that still has rows and the column that is in minor currency units. This is two to four weeks of analytics engineering that no model can do for you, and it is where the accuracy in a real deployment comes from.

How do you know whether a returned number is right?

Make it checkable rather than trusting it. Certified metrics carry canonical SQL so the agent agrees with the dashboard by construction. Deterministic verification catches empty results, magnitudes outside the stored plausible range for that metric, and filters that do not match what the question asked for. Then attach a provenance card (SQL, filters applied, row count, underlying table freshness and the run id) beside the number rather than behind a tooltip, so a reader can audit it in five seconds.

Should the agent generate the chart image?

No. Derive a chart specification deterministically from the result shape and column types (one date column and one numeric column becomes a line chart with the date on x, formatted in the metric's declared unit) and let your existing chart library render it. Letting a model choose axes and labels introduces truncated y-axes, invented units and mislabelled series, which are invisible to every check you have and highly visible in a board deck.

How long does it take to build a data analyst agent?

About eight engineer-weeks for a two-person team, plus two to four weeks of an analytics engineer's time on the semantic layer running in parallel, and that parallel track is the real critical path, because it needs someone who knows which of your four revenue tables finance actually uses. Build the sandbox and journal in week one, before the SQL generation, and scope version one to twenty certified metrics and four approved dimensions rather than promising coverage of the whole warehouse.

Ready to talk numbers?

Twenty minutes, straight to the engineer. No sales rep, no deck.