Schema drift is silent by default. Here is the governed loop that makes it visible, actionable, and resolved, without requiring manual vigilance to catch it first.
A semantic layer is easy to ship. What is hard is trusting it twelve months later. The warehouse has changed dozens of times. Backend engineers renamed columns. A status enum was split in two. New dimension attributes appeared that nobody told the data team about. The queries are still running. But are the numbers still right?
SpeedDelivery's data platform team maintains semantic models for three domains: delivery operations, finance, and consumer growth. Each model references a dozen tables owned by different engineering teams, on different release cycles, with no obligation to notify the data platform team when something changes.
This is the normal state of affairs at any organization operating at speed. A backend engineer renames total_fee to gross_amount as part of a payment system refactor. A data engineer splits the delivery_status enum into driver_cancelled and consumer_cancelled because the operational distinction finally matters. A new zone_tier column appears in zone_dim that three existing dashboards should be grouping by, but aren't. None of these changes break any queries immediately. The SQL still executes. The numbers just stop being right.
In a world without a semantic layer, the blast radius of any schema change is bounded. Each analyst owns their own SQL, so when a column breaks their query they notice immediately and fix it. With a centralized semantic layer, the blast radius inverts. One stale column reference in the physical layer corrupts every metric that depends on it, across every perspective and every team that inherits from it. The centralized model that was the source of consistent, trustworthy numbers becomes the source of consistently wrong ones, and nobody knows until a stakeholder runs a comparison.
The maintenance model that fails at scale is reactive: somebody notices a number looks wrong, files a ticket, a data engineer investigates, finds the stale reference, updates the YAML, opens a pull request, gets it reviewed, and merges it. By then the model has been stale for days or weeks. The investigation consumes hours that should have been spent on something else. And the damage, decisions made on wrong numbers before anyone noticed, is invisible and irreversible.
The model does not need better incident response. It needs a system that watches the warehouse continuously, detects drift the moment it happens, and initiates a governed update without requiring a human to notice it first.
"A semantic layer that cannot detect when the warehouse changed underneath it is not a governed system. It is a snapshot that is aging."
Every governed update follows the same cycle. The warehouse changes. The drift detector notices. A severity classifier routes the change. An agent builds a draft update using every available source of context. The draft lands in the Studio, where it is validated and reviewed. An owner approves or edits it. The change is published as a versioned, auditable revision. The model snapshot is updated. The detector's baseline advances. The loop closes and restarts.
What makes this a loop rather than a pipeline is the baseline update at the end. After each publish, the drift detector's snapshot of what the model expects is updated to match what was just approved. Future drift events are relative to the new baseline. A column that was added and approved stops generating noise. A column that was renamed and corrected no longer appears as a broken reference. The detector's attention stays focused on genuine new drift, not resolved history.
The drift detector runs on a schedule against every physical table registered in the semantic layer. For each table it fetches the live schema from the warehouse and compares it against the last approved snapshot stored in the model registry. The comparison is not just structural; it is semantic. The detector does not merely ask "did a column appear or disappear." It asks "did the meaning of a column change in a way that invalidates model definitions that depend on it."
Four categories of change trigger a drift event. Each carries an inherent severity level that determines how the system responds before any human sees it.
zone_tier column in zone_dim might belong in three models. If relevance is detected, severity is elevated to medium.total_fee as a metric source breaks silently the moment that column becomes gross_amount. This is always high severity and the affected models are identified immediately.delivery_status split is a textbook case: the model's canonical value list becomes incomplete, every filter that matches against the old list silently under-counts, and the backward-compatibility question (do existing queries break?) requires query history to answer.VARCHAR becoming a DECIMAL often represents a long-awaited cleanup; a DECIMAL becoming a VARCHAR is almost always an incident. Type changes always prompt a closer look regardless of severity classification.Every drift event is recorded with the affected table, the specific columns involved, the category and severity, the timestamp, and a full before-and-after snapshot. The models that reference the affected columns are listed explicitly. This record is the primary input the agent uses when building the draft, and the primary thing a dataset owner reviews when approving it.
drift_event: id: drift_20260314_delivery_fact table: delivery_fact detected: 2026-03-14T04:12:00Z changes: - type: column_rename severity: high from: total_fee to: gross_amount models_affected: - delivery_operations # 3 metric formulas reference total_fee - delivery_finance # revenue, net_revenue, commission routing_decision: blocked # high severity → owner approval required - type: enum_value_change severity: medium column: delivery_status before: [pending, accepted, in_transit, delivered, cancelled] after: [pending, accepted, in_transit, delivered, driver_cancelled, consumer_cancelled] retired: [cancelled] added: [driver_cancelled, consumer_cancelled] query_history_signal: filter_on_cancelled: 847 # queries in last 90d last_seen: 2026-03-13 # as recent as yesterday routing_decision: review_required # high compat risk escalates medium
The query_history_signal field is populated by the detector before the draft is built. It tells the agent, and the reviewing human, how many live queries were filtering on the retired value. 847 is a high-frequency signal. It means the enum split is not a minor cleanup; it is a backward-compatibility event that will silently change results for any query that was relying on cancelled to capture all cancellations. That context shapes both the draft and the routing decision.
The Studio is the surface where model evolution happens in the open. It is not a YAML editor on a laptop. It is a shared workspace where the data platform team and dataset owners can see every open drift event, inspect the agent's proposed response, run validation checks, and publish approved changes, all without leaving the tool and without touching a file directly.
The Studio has three panels. The drift inbox on the left shows every open event sorted by severity, with high-severity items flagged in amber. Clicking an event loads the agent-generated draft into the center editor, with diff markers showing exactly what changed and why, each change accompanied by the evidence that drove it. The validation panel on the right runs automatically when a draft is loaded: it checks that every formula resolves against the updated schema, verifies canonical value lists are complete, identifies downstream dashboards and saved queries that reference the changed columns, and surfaces any test failures. Nothing moves to the publish step until the relevant validation checks are green.
The warning about 847 queries using the retired enum value is surfaced as a warning, not a blocker. The model update can be published; the downstream queries do not need to be fixed before the model is correct again. The compatibility summary that ships with each publish gives dashboard owners the information they need to update their own filters at their own pace. This matters because holding the model update hostage to every downstream cleanup creates a backlog that nobody clears, and the model stays stale while waiting.
The agent does not guess. When it builds a draft, it assembles a context from three grounding sources before proposing a single change. Each source contributes something the others cannot: the drift event record provides the structural facts, query history reveals the behavioral consequences, and internal documents provide the authoritative intent behind a column's definition.
This is the foundation. The agent knows exactly which table changed, which columns are affected, what the before and after state looks like, and which model definitions reference those columns. For a column rename it has the old name and the new name. For an enum split it has the full set of retired and added values, the column's historical value distribution from the warehouse, and the query history signal showing how many live queries filtered on each value. This context is deterministic and complete; the agent does not need to infer what changed.
Query history is the most important grounding signal for backward-compatibility decisions. SpeedDelivery's semantic layer logs every query that passes through it: the perspective, the metrics requested, the dimensions sliced, the filters applied. This history is the closest thing to a specification for what the model needs to support. If delivery_status = 'cancelled' appears in 847 queries over the past 90 days, that is strong signal that retiring the value is a breaking change with significant downstream consequences, not a minor cleanup. The agent uses frequency and recency together: a high-frequency filter from a year ago carries less weight than a moderate-frequency filter from last week.
The agent has access to SpeedDelivery's internal document corpus: engineering RFCs, data dictionaries, Slack threads indexed from incident channels, and runbooks. When the drift event record references a column that also appears in a recent document, the agent pulls that document as context. The gross_amount rename was part of a broader payment system RFC that also clarified the distinction between platform fee and restaurant commission, and updated the definition of net revenue. The agent does not just rename the formula; it updates the metric's description field and disambiguation hints to match the RFC's language, because that language is now the authoritative definition.
agent_context: drift_event: drift_20260314_delivery_fact query_history: window: 90d column_usage: total_fee: in_metric_formula: 6 # models using it in a formula in_filter: 0 # nobody filters on it directly: safe rename delivery_status: filter_value_cancelled: 847 # high: backward-compat decision required filter_value_delivered: 312 filter_value_in_transit: 45 backward_compat_risk: high documents: - id: RFC-2026-014 relevance: high key_claims: - "total_fee renamed gross_amount for clarity with finance team" - "gross_amount = all fees before deductions" - "net_revenue definition unchanged, still post-deduction" - id: DATA-DICT-delivery_status relevance: medium key_claims: - "cancelled split: driver_cancelled + consumer_cancelled" - "most dashboards should treat both as cancellations" proposed_actions: - action: rename_formula_column metric: gross_revenue from: SUM(total_fee) to: SUM(gross_amount) also_update: description # align wording to RFC-2026-014 confidence: high - action: update_canonical_values dimension: delivery_status add: [driver_cancelled, consumer_cancelled] retire: [cancelled] confidence: high - action: add_derived_dimension name: any_cancellation expr: "status IN ('driver_cancelled','consumer_cancelled')" reason: backward_compat_shim confidence: medium note: "team may prefer different naming, flagged for review" confidence: high review_flag: true # compat risk overrides confidence on the enum change
The agent marks its own confidence. A draft where all evidence is consistent and the change is mechanical, a rename with a clear one-to-one mapping and an RFC that confirms intent, is marked high_confidence. A draft where the query history signals high backward-compatibility risk escalates to review_flag: true regardless of confidence, because the judgment call about how to handle 847 affected queries belongs to a person, not a language model. This escalation is not a failure state. It is the system correctly identifying where the human's judgment adds value.
The agent is good at synthesis. It is not good at judgment calls that require domain knowledge, stakeholder context, or business intuition that does not exist in any document or query log. Understanding which cases belong to a human, and making that boundary explicit in the draft, is as important as any of the synthesis work the agent does.
Conflicting definitions. When an RFC says one thing about a column and a data dictionary says another, the agent cannot resolve the conflict. It can surface both claims, note the contradiction, and flag the draft for human review. What it cannot do is decide which source is authoritative. That decision requires someone who knows which document reflects the current agreement, and which one is out of date.
High-frequency backward-compatibility breaks. When 847 queries filtered on a retired enum value, someone needs to decide whether to create a backward-compatibility shim (a derived dimension that unions the new values), retire the old filter pattern entirely, or notify all downstream users and give them time to migrate. Each choice has different consequences for different teams. The agent can model all three options in the draft, but it cannot pick the right one.
Type changes with business implications. A column changing from DECIMAL(12,2) to DECIMAL(18,4) is probably an improvement and can be approved mechanically. A column changing from INTEGER to DECIMAL suggests precision was previously being lost, which means historical values in the model may have been subtly wrong, and the right response might be a metric revision rather than just a type update. The agent flags this but cannot determine the business impact.
Every case that exceeds the agent's judgment boundary is marked explicitly in the draft. The agent does not silently guess when it is uncertain. It surfaces a structured note explaining what it knows, what it does not know, and what information the reviewing human would need to resolve the ambiguity. A draft with a clear uncertainty note is more useful than a draft that looks confident but has a hidden assumption baked in.
The agent's value is in compression: turning a one-hour investigation into a five-minute review. It reads the drift event, pulls the relevant RFC, analyzes query history, identifies affected models, and presents a structured draft with supporting evidence. The reviewing human still needs domain expertise. They still decide whether the proposed formula change is right for their business. But they are correcting and approving a well-evidenced draft, not starting from a blank page.
The agent synthesizes. The human judges. Neither can do the other's job.
Not every drift event needs a human to approve it before anything changes. A blanket "everything requires sign-off" policy sounds rigorous, but in practice it creates a backlog that grows faster than anyone can clear it, and the model stays stale while waiting for attention. The routing logic is consequentialist rather than bureaucratic: route by the actual risk of getting the change wrong, not by the category of change.
The routing for high-severity changes is absolute. A broken column reference that would corrupt metric formulas in production is never auto-applied regardless of how confident the agent is. Confidence is earned over many correct drafts; trust is destroyed by one silent error that reaches a stakeholder's dashboard. The cost of requiring one approval is low. The cost of skipping it is not.
The staging window for medium-severity, high-confidence changes is the key operational feature. It allows the model to move forward on routine updates (a new enum value, a newly relevant column) without blocking on approval queues, while still giving owners the ability to catch anything the agent got wrong before it reaches production queries.
The dataset owner is the person or team responsible for the source table. At SpeedDelivery, the owner of delivery_fact is the logistics data team. They are the authoritative source on what any column in that table means, and they are the right people to verify that the agent's draft reflects that meaning correctly.
When a draft is routed for owner validation, the owner sees three things: the diff between the current model and the proposed draft, the agent's full reasoning (which sources it used, what signals drove each decision, and where its confidence was lower), and the validation panel results. They can approve as-is, edit the draft directly in the Studio, or reject it with a note. Rejection notes are not discarded; they feed directly back into the agent's context for the next draft on the same model.
The publish step is not a merge button. It is a versioned operation: a new revision of the model is created, the full test suite is run against the updated definitions, and a before-and-after snapshot is stored for audit purposes. Every downstream consumer (dashboards, saved queries, scheduled reports) receives a notification that the model changed, accompanied by a compatibility summary.
publish: model: delivery_operations version: 2.4.1 approved_by: logistics-data-team published_at: 2026-03-14T11:34:00Z drift_event: drift_20260314_delivery_fact changes: - metric: gross_revenue formula: SUM(total_fee) → SUM(gross_amount) description_updated: true # aligned to RFC-2026-014 language - dimension: delivery_status canonical_values: added driver_cancelled, consumer_cancelled; retired cancelled - dimension: any_cancellation type: derived note: "backward-compat wrapper: TRUE when driver_cancelled OR consumer_cancelled" compatibility: unaffected_queries: 891 result_change_possible: 47 # filtered on retired 'cancelled' value broken_references: 0 # any_cancellation shim covers all cases tests: passed (86/86)
The 47 queries flagged as "result change possible" are the ones that filtered on the retired cancelled value. They are not broken (the derived any_cancellation dimension means they still execute) but their semantics have changed: they now capture both driver_cancelled and consumer_cancelled rows. Whether that is the right behavior for each of those 47 queries is a question each query's author needs to answer. The compatibility summary gives them the information they need to make that decision. The model is not responsible for making it for them.
Every rejected draft carries a note. The owner explains what was wrong: the agent proposed updating the wrong formula, or the RFC it cited was superseded, or the backward-compatibility shim it generated did not match the team's naming conventions. These notes are not just corrections for this draft; they are constraints on future drafts for the same model. The agent accumulates a rejection history per model, and that history shapes every draft it generates afterward.
The practical effect compounds quickly. After a model has gone through two or three drift cycles, the agent's drafts become substantially more accurate because they are constrained by everything the owner has already told it is wrong. The first draft for a new model requires careful review. The tenth draft for a well-maintained model often requires only a quick scan. The system gets easier to maintain the more it is used, which is the opposite of the pattern most semantic layers follow.
rejection_record: draft_id: draft_20260314_delivery_fact_v1 model: delivery_operations rejected_by: logistics-data-team rejected_at: 2026-03-14T10:14:00Z reason: | The any_cancellation shim name is wrong. Our team uses is_ prefix for all boolean flag dimensions. Should be is_cancelled. corrections: - field: dimensions.any_cancellation.name proposed_value: any_cancellation correct_value: is_cancelled stored_as_constraint: scope: delivery_operations rule: "boolean flag dimensions use is_ prefix" applies_to: dimension.name effective: 2026-03-14 # Draft v2 is generated immediately with the constraint applied: # dimensions[0].name: is_cancelled ← correct # Owner approves v2 in 40 seconds. Constraint stays in context permanently.
Owner approvals also feed back, not just rejections. When an owner approves a draft that included an unusual choice (a non-standard naming convention, a disambiguation hint phrased in domain-specific language, a derived dimension that the agent invented rather than copied from the existing model) that approval is recorded as positive signal. The agent learns which choices the owner validates without being told to, and applies the same patterns in future drafts. Over time, the agent develops a model-specific style that reflects the team's preferences without anyone ever explicitly writing them down.
"The feedback loop is what separates a system from a tool. A tool is equally hard to use every time. A system gets easier the more context it accumulates."
The biggest surprise building this system was realizing that a semantic layer's greatest strength, centralizing metric definitions, is also its greatest operational risk. One stale reference corrupts everything that depends on it, silently. Before building any of the maintenance machinery, that risk needs to be the design constraint. The drift detector, the severity routing, the publish audit trail: all of it exists specifically because centralization raises the stakes of every schema change.
I initially assumed internal documents would be the primary grounding signal for the agent. In practice, query history turned out to be more operationally important, because it answers the question documentation cannot: who is actually relying on this, and how often? A retired enum value in a column nobody filters on is a cosmetic change. The same change in a column that appears in 847 active queries is a migration event. That distinction is invisible in any document. It is only visible in the query log.
The first version of the routing logic was categorical: all column renames require approval, all new columns are auto-approved. This produced a backlog of approvals for obviously correct renames and auto-approved new columns that turned out to be highly relevant to existing models. Switching to consequence-based routing (severity determined by impact on existing model definitions, confidence determined by evidence quality) dramatically reduced unnecessary approval requests while catching the changes that actually needed attention.
Teams start trusting the semantic layer on the day it is first deployed. They keep trusting it based on what happens when the warehouse changes. A model that handles drift correctly (notifying downstream consumers, providing a compatibility summary, publishing a versioned audit record) earns more trust with each update than a model that was perfectly correct at launch but handled its first schema change poorly. The publish step is not an operational detail. It is the primary trust-building event in the system's lifecycle.
Every component described in this article (the drift detector, the severity classifier, the agent's three-source context assembly, the Studio, the staged review workflow, the versioned publish, the feedback loop) exists to answer the same question: is the number this model produces today based on the same definition it was based on last month?
That question does not have a permanent answer. The warehouse will change again tomorrow. Another column will be renamed. Another enum will be split. Another team will add a dimension attribute that three existing models should expose but currently don't. The right response to that reality is not more manual vigilance. It is a governed loop that notices, drafts, routes, validates, and publishes, and gets better at each step as the feedback accumulates.
A semantic layer that was correct at launch and has no mechanism for staying correct is a snapshot. A semantic layer with a governed maintenance loop is a system. The difference is not whether it fails: every model eventually diverges from a warehouse that keeps moving. The difference is whether the failure is silent or visible, whether it persists for weeks or resolves in hours, and whether the fix is a panicked manual correction or a governed, auditable update. That is what a maintenance loop buys.