← All Articles
Semantic DataXpress  ·  Platform Deep Dive

A Semantic Layer That Keeps Itself Current

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.

Narendra Devarasetty  ·  2026
14 min read

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?

Key takeaways
In this article
  1. The staleness problem
  2. The maintenance loop
  3. Schema drift detection
  4. The Studio
  5. How the agent builds the draft
  6. What the agent cannot decide
  7. Human in the loop
  8. Dataset owner validation and publish
  9. The feedback loop
  10. What I learned building it

The staleness problem

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

· · ·

The maintenance loop

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 Maintenance Loop: full cycle from warehouse change to updated snapshot
IN OUT WAREHOUSE schema evolves continuously detects DRIFT DETECT compares snapshot to live schema routes CLASSIFY severity · confidence HIGH / MED / LOW drafts AGENT DRAFT 3-source context event · history · docs to studio STUDIO review draft · validate · owner sign-off blocked on HIGH severity until approved publishes PUBLISH versioned · tested compat summary sent updates SNAPSHOT baseline advances loop restarts loop Rejected drafts feed back into the agent's context · each cycle improves the next
· · ·

Schema drift detection

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.

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 record: delivery_fact, two simultaneous changes
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

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.

Studio: drift inbox · agent draft · validation panel
DRIFT INBOX 3 open delivery_fact HIGH column_rename total_fee → gross_amount 2 models affected delivery_fact MED enum_value_change delivery_status split 847 live queries review required zone_dim LOW column_added zone_tier · model relevant delivery_operations.yaml high conf metrics: - name: gross_revenue label: Gross Revenue formula: SUM(gross_amount) # renamed · RFC-2026-014 - name: cancellation_rate canonical_values: - driver_cancelled - consumer_cancelled # retired: cancelled (847 queries) dimensions: - name: any_cancellation type: derived # compat shim modified added Approve Edit Reject VALIDATION ✓ formula resolves (gross_amount) ✓ column exists in table ✓ canonical values complete ⚠ 847 queries use retired value ✓ tests pass (12/12) ✓ 0 broken references DOWNSTREAM IMPACT 3 dashboards 2 saved queries · 1 scheduled report compat summary sent on publish Publish v2.4.1

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.

· · ·

How the agent builds the draft

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.

The drift event record

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

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.

Internal documents

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.

full agent context assembled for the delivery_fact drift event
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.

· · ·

What the agent cannot decide

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 right division of labor

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.

· · ·

Human in the loop

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.

Routing decision flow: severity × confidence → response path
DRIFT EVENT severity · confidence · affected models HIGH MEDIUM LOW HIGH SEVERITY broken ref · retired value MEDIUM SEVERITY new enum · new column LOW SEVERITY new column · no model impact high confidence review required High confidence consistent evidence, clear mapping Review required conflict · high compat risk BLOCKED owner must approve nothing applied until then AUTO-STAGING applied to staging · owner notified with review window STUDIO QUEUE queued for review · owner must approve before staging LOG + SURFACE passively visible in Studio no action required

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.

· · ·

Dataset owner validation and publish

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 record: delivery_operations v2.4.1
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.

· · ·

The feedback loop

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: stored as a constraint on future drafts
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."

· · ·

What I learned building it

LESSON 01
The blast radius of centralization is the first thing to design for

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.

LESSON 02
Query history is more useful than documentation for backward-compatibility

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.

LESSON 03
Route by consequence, not by category

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.

LESSON 04
The publish step is where trust is built, not at launch

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.

· · ·

A model that earns ongoing trust

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.

← Back to all articles