The Semantic Layer Is What Makes an LLM Useful on Your Data

Juan Piaggio · 2026-08-12 · 7 min read · ai · architecture · data

Give a capable model your schema and a question, and it will write you SQL. The query will run. The number will look plausible. And roughly a third of the time it will be quietly, confidently wrong — not because the model cannot write SQL, but because your schema does not contain the information needed to answer the question correctly.

That gap is what a semantic layer fills. It is the place where "active project", "cycle time", and "last sprint" have exactly one meaning, and where that meaning is enforced rather than remembered.

Schema is not meaning

A column named status tells the model that a string exists. It does not say that ARCHIVED still counts as active for billing purposes, that three of the twelve possible values were deprecated in March, or that rows written before a migration use a different convention entirely.

Every organization carries a pile of this knowledge. Which timestamp represents the event versus when the row was written. Whether a "closed" ticket includes ones closed as duplicates. Analysts hold it in their heads and apply it without noticing. A model has no access to any of it, so it does the only thing available: it infers from names, and names lie.

The failure mode this produces is the dangerous kind. Not an error, not a refusal — a number, formatted nicely, off by fifteen percent, delivered with the same tone as a correct one.

What the layer actually holds

A semantic layer is a small, boring, deliberately curated description of your domain. The useful ones contain:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 250" role="img">
  <title>One definition of every entity and metric, enforced on the way in and on the way out</title>

  <rect x="1" y="4" width="793" height="44" rx="10" fill="var(--primary-50)" stroke="var(--primary-600)" />
  <text x="397" y="31" text-anchor="middle" font-size="14" fill="var(--ink-1)">Natural language: “Which initiatives slipped last sprint?”</text>

  <path d="M397 48 V64" stroke="var(--text-muted)" stroke-width="1.5" fill="none" />
  <path d="M392 64 l5 10 5 -10 z" fill="var(--text-muted)" />

  <rect x="1" y="78" width="793" height="92" rx="10" fill="var(--sand-1)" stroke="var(--border)" />
  <text x="397" y="99" text-anchor="middle" font-size="11" fill="var(--text-muted)">Semantic layer — definitions, grain, synonyms, permissions</text>

  <rect x="14" y="110" width="250" height="48" rx="8" fill="var(--primary-50)" stroke="var(--primary-600)" />
  <text x="139" y="140" text-anchor="middle" font-size="14" fill="var(--ink-1)">Entities</text>

  <rect x="275" y="110" width="250" height="48" rx="8" fill="var(--primary-50)" stroke="var(--primary-600)" />
  <text x="400" y="140" text-anchor="middle" font-size="14" fill="var(--ink-1)">Metrics</text>

  <rect x="536" y="110" width="244" height="48" rx="8" fill="var(--primary-50)" stroke="var(--primary-600)" />
  <text x="658" y="140" text-anchor="middle" font-size="14" fill="var(--ink-1)">Scope rules</text>

  <path d="M397 170 V186" stroke="var(--text-muted)" stroke-width="1.5" fill="none" />
  <path d="M392 186 l5 10 5 -10 z" fill="var(--text-muted)" />

  <rect x="1" y="200" width="793" height="44" rx="10" fill="var(--sand-1)" stroke="var(--border)" />
  <text x="397" y="227" text-anchor="middle" font-size="14" fill="var(--ink-1)">Scoped query · governed tool call · audited result</text>
</svg>

Note what the layer is not: it is not a copy of your schema with comments added. It is smaller than your schema, on purpose. Most columns are noise for most questions, and a model given fifty tables will find a way to use them.

Question Raw schema access Semantic layer
What does "active" mean? Inferred from a column name Defined once, versioned
Which join is correct? Whichever keys look compatible Only permitted paths exist
Is tenant filtering applied? Only if the prompt remembered Structurally unavoidable
Why is this number different? Read the generated SQL Diff the metric definition

It is also the layer your tools should speak

The same argument applies to agents that act, not just agents that report. A tool exposed to a model should accept domain concepts — an initiative, a sprint, a transition — rather than raw identifiers and free-form filter strings.

This is the design our MCP tool hub follows. External agents get eight tools defined in domain terms, and no tool accepts a workspace or organization argument at all. The tenant anchor comes from the verified token, and every identifier passed in must be proven to belong to that workspace before it is used. A model cannot ask for another tenant's data because the vocabulary contains no way to express the request.

That is the property worth designing for. Prompt-level instructions are a request; a vocabulary that cannot express the wrong thing is a guarantee. The same reasoning is why we treat an identifier as unproven until scoping confirms it — an id looks self-validating and is not, and the gap between those two things is where the interesting bugs live.

Ambiguity should abstain, not guess

Some questions genuinely cannot be answered from the layer. "How are we doing on velocity?" has three plausible readings, and picking one silently is how a model loses a user's trust in a single interaction.

The right behavior is the same one we use for agent proposals generally: when confidence sits below the floor, abstain and ask. A clarifying question costs one round trip. A confidently wrong number costs the credibility of every number that follows it, including the correct ones. The semantic layer makes abstention cheap, because it knows what it does not define — an unmapped term is a fact, not a judgment call.

Who owns it

The layer only works if it has a keeper. Left to accumulate, it drifts: definitions get added for one dashboard, entities get duplicated with slightly different filters, and within a year you have reproduced the original problem inside the thing built to solve it.

Treat it like code, because it is. Version control, review, and a release note when a definition moves — so "why did this number change?" is answered by a diff rather than an investigation.

The takeaway

Models are good at language and indifferent to your conventions, which is why handing one a raw schema produces answers that are fluent and unreliable. A semantic layer gives the model a curated vocabulary — entities, single-definition metrics, real-world synonyms, permitted joins, and non-optional scope rules — and gives you one place to fix a definition for every consumer at once. Build it for your tools as well as your queries, so an agent's action vocabulary is as governed as its reporting vocabulary. Let it abstain when a term is undefined. Then version it, review it, and give it an owner. It is the least glamorous component in an AI data stack, and the one that decides whether anybody trusts the output.

← All Field Notes