The Ops Atlas xOps School

Data & AI Ambiguous / not standardised Sources verified

AnalyticsOps

AnalyticsOps has no standard definition. On this page it means operating the layer where people consume data: versioned metric definitions, a semantic layer that serves them, and BI content that is certified, monitored and retired, so each important metric has one trusted definition.

Key takeaways

  • The label is ambiguous: Open Data Group (renamed ModelOp in 2019) and Teradata used "AnalyticOps" for deploying analytic models; an analyst and a BI-testing vendor use the term for the analytics last mile. This page takes the last-mile meaning.
  • The core problem is drifting copies of definitions, not broken pipelines: Uber traced 6.53M versus 6.20M sessions for the same city and period to stale filters in one tool's query.
  • Treat each important metric as a product with a written definition, a business owner, a version, a certification status and a list of consumers.
  • Put definitions where every consumer can reach them: the BI tool's model if there is one tool; a tool-independent semantic layer if several tools, notebooks or AI assistants need the same numbers.
  • Change a certified definition like an API: version it, measure its effect, tell consumers, provide a back series. Correct errors promptly and say how big they were.
  • Retire content on usage evidence (views and scheduled deliveries) and only after notice. Export usage logs before they age out; certified content can also be unused.

AnalyticsOps has no agreed definition. On this page it means operating the layer where people consume data: metric definitions, the semantic layer that serves them and the dashboards built on top, so every important number has one trusted definition, an owner and a known freshness, and unused content is retired. DataOps makes the tables correct and on time; AnalyticsOps makes “revenue” mean the same in the board pack, the sales dashboard and the spreadsheet.

For analytics engineers and BI developers, analytics leaders who decide what to certify and retire, business owners of KPIs, and beginners who wonder why two dashboards disagree.

Item Summary
Term status Ambiguous. No standard, body or certification; four uses, two spellings. A 2021 prediction that one-third of organisations would adopt it by 2024 was never measured, as far as this page found.[4]
Goal One trusted definition for each important metric, and a dashboard estate that people use and trust.
In scope Metric definitions and lifecycle; the semantic layer; certifying, monitoring and retiring BI content; governed self-service; report freshness; conflicting-number incidents.
Out of scope Pipelines and data tests → DataOps; who may see which rows → DataSecOps; which KPIs the company runs on → BizOps; automated decisions → DecisionOps; model lifecycles → MLOps.
Prerequisites SQL; basic dimensional modelling (facts, dimensions, grain); the DataOps page.
Key outputs Metric catalogue and cards; certified semantic models; change log; usage-based content review; conflict runbook.

When analysis is decentralised, the same metric gets defined many times. Uber’s metrics team wrote that “we often observed multiple versions of metrics defined or generated in different channels for the same business logic, which could be inaccurate or misleading”. In one example, two internal tools showed “Shopping Sessions” for the same city and period as 6.53M and 6.20M; the cause was one tool’s “stale filters in their static query”.[6] At Airbnb, when the CEO asked which city had the most bookings, “Data Science and Finance would sometimes provide diverging answers using slightly different tables, metric definitions, and business logic”, and “trust from decision makers degraded”.[7]

No pipeline needs to break for this. Copies of one definition drift apart, and correct data then yields conflicting numbers when a filter, time zone, grain, currency rule or refund treatment differs. The costs: meetings spent reconciling, decisions on the wrong number, and a growing estate of dashboards that is refreshed and paid for but rarely opened.

Operating reality Before After
Definition of “revenue” Inside each dashboard’s query One versioned definition with an owner, reused by every tool
Two numbers disagree Escalation, side-by-side spreadsheets Cards compared in minutes; one canonical metric
Changing a definition Silent edit; consumers notice later Pull request, impact list, measured size, notice, back series
Dashboard estate Grows indefinitely Reviewed on usage; archived after notice
Trust and freshness “Ask the analyst” On the content: badge, owner, version, “data as of”

What AnalyticsOps is not: naming and boundaries

Link to section “What AnalyticsOps is not: naming and boundaries”
  • Not DataOps renamed. DataOps asks whether data arrives correct and on time; this page asks whether everyone uses one definition of each number.
  • Not a product. Buying a semantic layer does not decide what “active customer” means; people do.
  • Not choosing KPIs. That is BizOps; this page governs how KPIs are defined, served and changed.
  • Not a central report factory. Tableau’s governance guidance warns against “recreating a ‘report factory’ delivery model” and advises setting “goals and dates to evolve beyond the centralized governance model”.[8]
  • Not operational analytics. Analysing operational data (logistics, IT events) is a use of analytics, not its operation.

Where AnalyticsOps comes from and where it sits

Link to section “Where AnalyticsOps comes from and where it sits”

The practice is older than the label.

When Milestone
Before the label Metrics defined once inside one BI tool, for example LookML models kept in Git[9]
2019 “Analytics engineer” named as a role using “version control and continuous integration”[10]
2020–2021 Tool-independent metric platforms: Airbnb’s Midas (2020) and Minerva (2021); Uber’s uMetric (2021)[11][7][6]
2021–2022 The label appears for this meaning, from an analyst and a vendor[4][5]
2023 dbt Labs agrees to acquire Transform, “founded by former Airbnb data scientists”, to merge MetricFlow into the dbt Semantic Layer[12]
2026 Open Semantic Interchange, in the Apache Incubator, is renamed Apache Ossie (announced July);[13] warehouses offer native semantic objects[14][15]
Term Meaning on this page
Simple metric (measure) An aggregation over one column; dbt’s latest spec replaces “measures” with simple metrics.[16]
Dimension, entity, grain An attribute to group or filter by; a join key (customer, order); what one row represents.
Additivity Whether values can be summed across a dimension. Revenue sums across days; daily distinct customers do not add up to a monthly count, and balances do not sum over time (dbt’s non_additive_dimension).[17]
Semantic layer Metric, dimension and join definitions that tools query instead of raw tables; it generates the SQL.[18]
Semantic model Power BI: the model behind reports; dbt: the semantic annotations on a model.
Definition card This page’s term: a versioned, business-readable definition with owner, status and consumers.
Certification A reviewer’s approval that content meets agreed standards, shown as a badge; tools differ in what can carry one (table).
Back series, restatement Past values recomputed under a new definition, so charts show no step that is only a definition change; republishing reported numbers with the size of the change.
Exposure dbt’s declaration that a dashboard or application depends on models or metrics.[19]
Tier This page’s criticality class: 1 board, external or regulated; 2 departmental; 3 team and self-service.
Definition drift One name whose definitions diverge across tools or over time.
Freshness promise When viewers can expect current data: “X% of data processed in Y”.[20]

Mental model: a metric is an API between data producers and decision makers. The semantic layer is the interface, the card the contract, dashboards and spreadsheets the clients; a change is compatible (a label) or breaking (a filter, grain or formula).

Figure 1. Where AnalyticsOps sits between data producers and decision makers
BizOps: which KPIsDefinitions with owner andversionDataOps: tested tablesSemantic layerDataSecOps: access policiesDashboards on certifiedmodelsSpreadsheets, notebooksAI assistantsDecisionOpsDecision makers
Diagram source (Mermaid)
flowchart TB
  BZ["BizOps: which KPIs"] --> DEF
  DO["DataOps: tested tables"] --> DEF
  DEF["Definitions with owner and version"] --> SL["Semantic layer"]
  DS["DataSecOps: access policies"] -.-> SL
  SL --> CC["Dashboards on certified models"]
  SL --> SS["Spreadsheets, notebooks"]
  SL --> AI["AI assistants"]
  SL --> DC["DecisionOps"]
  CC --> U["Decision makers"]
  SS --> U
  AI --> U
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class DEF,SL,CC core
  class BZ,DO,DS,SS,AI,DC,U neutral

Text equivalent: BizOps (which KPIs) and DataOps (tested tables) feed the definitions, which carry an owner and a version. The semantic layer serves them under DataSecOps’s policies (dotted) to dashboards on certified models, spreadsheets and notebooks, AI assistants, and DecisionOps; the first three reach decision makers. Green: AnalyticsOps parts. Grey: neighbouring pages, consumers outside the governed path, and decision makers.

These splits are this Atlas’s editorial boundaries, not industry definitions.

Neighbour Shared scope Its unique focus AnalyticsOps focus
DataOps Freshness, tests Pipelines, data contracts, data incidents Definitions, BI content, conflicting-number incidents
BizOps KPIs Which KPIs, operating cadence One governed definition per KPI and its dashboards
DecisionOps Decisions that use metrics Automated decision logic, audit The metric definitions those decisions consume
DataSecOps Access to data Row and column policies, masking Applying them consistently in the semantic layer and exports
ModelOps, MLOps The older “AnalyticOps” meaning Model lifecycle Metrics; Uber’s platform extends “a metric to a machine learning feature”[6]
ProductOps Product metrics Insight-to-roadmap process Definitions of activation, retention, engagement

No official AnalyticsOps manifesto exists; this charter is this Atlas’s proposal. Mission: every number that drives a decision has one definition, one accountable owner, a known freshness and a visible trust status.

  1. Define once, serve everywhere. Airbnb’s stated vision for Minerva was “define metrics once, use them everywhere”.[7]
  2. Definitions are code, reviewed like code. Minerva enforced “code review, static validation, and test runs”;[7] LookML projects are “typically version-controlled together through a Git repository”.[9]
  3. The business owns meaning; analytics owns encoding. Finance decides what “net revenue” includes; an analytics engineer encodes and tests it.
  4. Discipline at the core, flexibility at the edge. Microsoft’s phrase for managed self-service.[21]
  5. Change like an API. Planned changes get notice and a back series; errors are corrected promptly with their measured size, as the UK Code of Practice for Statistics asks of statistics producers.[22]
  6. Trust is visible on the content. Badge, owner, version and freshness appear where people read the number.
  7. Use decides the estate. What nobody views or receives is archived after notice, certified content included.

Non-goals: one dashboard for everything; banning spreadsheets; certifying everything (certification is slow; see Case 2).

Seven building blocks recur: a catalogue of definition cards (find a metric before creating one), a semantic layer (compute it one way for every consumer), change control, certification, lineage for impact, usage and delivery telemetry, and freshness signals. The tooling landscape maps them to products and core practices to owners.

The metric owner (finance, product, sales) is accountable for meaning; the analytics engineer encodes and tests; the BI content owner builds on certified definitions; the certifier reviews (Airbnb designates “Data Architects or Metrics Architects”;[11] see what each tool can certify); the platform admin runs tenant settings, usage export and archiving; data engineers (DataOps) own the tables. R responsible, A accountable, C consulted, I informed.

Duty Metric owner Analytics engineer BI content owner Certifier Platform admin
Define or change a tier-1 metric A R C C I
Certify a metric or content (R prepares; A approves) C R R A I
Approve a breaking definition change A R C C I
Publish a change notice A R I I C
Quarterly content review and archiving I C R C A
Respond to conflicting numbers A R R C I
Export and retain usage and delivery logs I I I I R/A

Each state means something to consumers; it lives in the definition’s metadata (a status key in Lab 2).

Figure 2. Lifecycle states of a governed metric
duplicate of an existing metricowner named, card writtenreview passedreviewed changesuccessor and noticenotice period ends, noconsumers leftProposedRejectedDraftCertifiedDeprecatedRetired
Diagram source (Mermaid)
stateDiagram-v2
  direction TB
  [*] --> Proposed
  Proposed --> Rejected: duplicate of an existing metric
  Proposed --> Draft: owner named, card written
  Draft --> Certified: review passed
  Certified --> Certified: reviewed change
  Certified --> Deprecated: successor and notice
  Deprecated --> Retired: notice period ends, no consumers left
  Rejected --> [*]
  Retired --> [*]

Text equivalent: A Proposed metric that duplicates an existing one is Rejected; otherwise it becomes a Draft once an owner and card exist, and Certified after review. A reviewed change creates a new Certified version. To remove it, the owner names a successor and sends notice (Deprecated); after the notice period, with no consumers left, it is Retired.

Dashboards follow a similar path: sandbox, promoted, certified, monitored, archived, deleted; in Tableau’s self-governing model, viewers “understand the delineation between certified and ad-hoc, sandbox content states”.[8]

  • Tiers set the controls. Tier 1 (board, investor, regulatory, pricing): certified only, change notice, monthly reconciliation. Tier 2 (department): certified models, lighter review. Tier 3 (team): any model, labelled uncertified.
  • Ceremonies. Weekly triage; monthly certification review; quarterly content review (GitLab’s cadence[23]); a governance review twice a year, as Tableau recommends.[8]
  • Decision rights. When two departments need different definitions under one name, a metrics council keeps one canonical metric and qualifies the other: “active customers (product, 28-day)”.
  • Self-service model. Microsoft describes three content ownership strategies (business-led self-service, managed self-service and enterprise) that most organisations mix, and suggests reserving certification for enterprise and managed self-service content.[21]

A data plane (tables, semantic layer, consumers) and a control plane (Git, CI, telemetry, review).

Figure 3. Reference architecture: definitions as code, one semantic layer, many consumers
Git: definitions and cardsCI: parse, validate, changegateSemantic layer runtimeTested tables from DataOpsDashboards on certifiedmodelsSpreadsheets, notebooksAI assistantsUsage and freshnesstelemetryCatalogue and content review
Diagram source (Mermaid)
flowchart TB
  G["Git: definitions and cards"] --> CI["CI: parse, validate, change gate"]
  CI --> SL["Semantic layer runtime"]
  T["Tested tables from DataOps"] --> SL
  SL --> BI["Dashboards on certified models"]
  SL --> X["Spreadsheets, notebooks"]
  SL --> AI["AI assistants"]
  BI --> TM["Usage and freshness telemetry"]
  X --> TM
  AI --> TM
  TM --> R["Catalogue and content review"]
  R --> G
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  class G,CI,R,TM decision
  class SL,BI core
  class T,X,AI neutral

Text equivalent: Definitions and cards live in Git; CI validates them and runs a change gate before they reach the semantic layer runtime, which reads tested tables from DataOps and serves dashboards on certified models, spreadsheets and notebooks, and AI assistants. Their usage and freshness telemetry feeds the catalogue and content review, whose decisions return to Git. Amber: control plane. Green: governed data plane. Grey: inputs and consumers outside the governed path (tested tables, spreadsheets and notebooks, AI assistants).

Responsibilities. Git keeps code and card together; CI rejects definitions that fail validation and metric or model-level changes without a version bump and change note (Lab 2); the semantic layer generates the SQL; telemetry answers “is anyone using this?” and “was the data on time?”. Model SQL and upstream tables are DataOps’s to test.

Trust boundary. Access rules live in the warehouse or the semantic layer (Cube: “Access control runs at the semantic layer, so the same policies apply to every consumer”[24]); direct connections and exports bypass the layer. Policy design belongs to DataSecOps.

Trade-offs. With one BI tool, its own model is the cheapest semantic layer; a transformation-layer one reflects dbt Labs’ view that “the metric definition is part of the modeling layer, rather than the business intelligence layer”.[12] See the decision framework.

End-to-end workflow: a metric change request

Link to section “End-to-end workflow: a metric change request”
Figure 4. Metric request and change workflow with the breaking-change path
YesNo, or a changeNoYesRequest: new or changedmetricAlready in catalogue?Point to existing metric; closeOwner approves definitioncardPull request; CI validates andlists impactBreaking change?Review and certifyVersion bump, measuredsize, notice, back seriesPublish, then monitor usage
Diagram source (Mermaid)
flowchart TB
  RQ["Request: new or changed metric"] --> DUP{"Already in catalogue?"}
  DUP -- "Yes" --> REUSE["Point to existing metric; close"]
  DUP -- "No, or a change" --> CARD["Owner approves definition card"]
  CARD --> CI["Pull request; CI validates and lists impact"]
  CI --> BRK{"Breaking change?"}
  BRK -- "No" --> CERT["Review and certify"]
  BRK -- "Yes" --> NOTE["Version bump, measured size, notice, back series"]
  NOTE --> CERT
  CERT --> PUB["Publish, then monitor usage"]
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  class RQ,CARD,CI,CERT,PUB,REUSE core
  class DUP,BRK decision
  class NOTE risk

Text equivalent: A request is checked against the catalogue; an existing metric is reused. Otherwise the owner approves a definition card, and code and card go into one pull request. CI validates, runs a change gate and lists affected dashboards. A breaking change first gets a version bump, a measured size, notice and a back series (red); then review, certification, publication and monitoring.

Happy path (illustrative): a tier-2 metric goes from request to certified in about a week. Exception path: a breaking tier-1 change waits out a notice period (this page suggests ten working days) and ships with its measured size and a back series. Do not skip the duplicate check: Uber reports that “there can be 10X or 100X instances spinned off for a popular metric”.[6]

Practice Trigger Owner Output Verification
Intake and duplicate check Request for a new number Analytics engineer Reuse, rename or new card No conflict (Lab 1)
Definition cards New or changed metric Metric owner Formula, filters, grain, additivity, time zone, version Owner sign-off
Certification Tier 1 or 2 content Certifier Badge and its meaning Criteria checklist; sample audit
Change management Certified definition changes Metric owner Version, measured size, notice, back series Change gate passes
Freshness signalling Every refresh Content owner “Data as of”, warnings Freshness attainment
Content review Quarterly Platform admin Archive list, new owners Active-content ratio
Conflict response Two numbers, one metric Metric owner Explanation, canonical metric Runbook closed
Self-service enablement New creators Analytics lead Templates, training[21] Team content on certified models
  1. Card. The owner writes the definition in business language (template); Midas design specs likewise start with “plain-text business definitions”.[11]
  2. Code and validate. The analytics engineer encodes the metric with owner, tier, status and version metadata, and CI validates it (Lab 2). A tier-1 metric also gets an assertion test or a reconciliation with its system of record.
  3. Review. A certifier who is not the author checks card, code, tests and consumers; one of Midas’s four reviews covers “the source of truth metric definitions implemented in Minerva”.[11]
  4. Publish. Badge, owner, version and consumers visible. Rollback: revert the pull request; the previous version stays.

There is no AnalyticsOps standard. Separate what binds from what helps:

  • Statutory only in its domain. Producers of UK official statistics “should apply” the Code of Practice for Statistics (Edition 3.0) under section 10 of the 2007 Act; accredited official statistics are “required under the Act to continually meet the standards of the Code”. Its principles are offered “for analysts and organisations both inside and outside of government”, and map onto metric governance: correct errors transparently, keep data “consistent over time”, announce planned method changes with “a consistent back series where possible”, flag statistics “being developed and tested”.[22]
  • Specifications. Apache Ossie (formerly Open Semantic Interchange), “an open specification for both semantic layer and ontology”, now in the Apache Incubator;[13] MetricFlow’s YAML spec for dbt.[18]
  • Vendor methodologies. Tableau Blueprint’s centralized, delegated and self-governing models;[8] Microsoft’s Fabric adoption roadmap.[21][25]
  • Reliability practice. Freshness and correctness objectives from the SRE Workbook.[20]
  • A design constraint. “When a measure becomes a target, it ceases to be a good measure” (Strathern, on what Hoskin called Goodhart’s law).[26]

Job first, product second. Representative options as of October 2026; plan limits change often.

Job Tool category Representative options Watch-outs
Define metrics as code Transformation-layer semantic layer dbt Semantic Layer with MetricFlow (Apache 2.0 engine)[18][27] Downstream tools need a dbt Starter, Enterprise or Enterprise+ account;[28] no cross-project references yet[16]
Standalone semantic layer Cube (open-source Cube Core)[24] Another runtime to operate and secure
Warehouse-native Snowflake semantic views; Databricks metric views[14][15] Ties definitions to one warehouse
BI-native model LookML; Power BI semantic models as PBIP (Power BI Project) text files; Tableau published data sources[9][29] Other tools cannot reuse them
Validate and deploy CI and release tooling dbt sl validate, mf validate-configs;[30] Looker Content Validator;[31] Fabric deployment pipelines The Content Validator “does not have an undo function”; each pipeline stage needs a Premium workspace[32]
Certify and signal Badges and warnings Certification (table below); Tableau data quality warnings[33] Warnings need Tableau Data Management
Measure usage and retire Usage telemetry, cleanup Power BI usage metrics and activity log;[34][35] Tableau Admin Insights;[36] Looker Unused content;[37] scripts on admin APIs[23] Short retention; deliveries counted differently (below)
Impact analysis Lineage dbt exposures;[19] BI-native lineage Undeclared consumers (exports, direct SQL) stay invisible

Usage data (as of October 2026). The Power BI activity log keeps “a maximum of 28 days”, usage metrics 30;[35][34] the Microsoft Purview audit log keeps Fabric and Power BI activity for 180 days under Audit (Standard) but needs an audit role and another API.[38][39] Tableau Admin Insights keeps 90 days, 365 with Advanced Management.[36] Power BI logs a subscription snapshot as a report view; Tableau lists subscriptions in a separate data source. Looker’s Unused content folder (Looker 26.14 or later; Looker (original) must be Looker-hosted) counts an active schedule as use and does not list public, favourited or API-used content (such as Connected Sheets) as unused.[34][36][37] Export usage and delivery data daily.

As of October 2026:

Tool Certified badge on Dashboards and reports Who certifies
Tableau Published data sources; with Data Management, virtual connections; with Catalog too, databases, tables, files[40] No badge: Blueprint certifies workbooks by promoting them to a production project[41] Administrators; project owners or leaders who can publish
Fabric, Power BI “Any Fabric or Power BI item except Power BI dashboards”[42] Reports and semantic models yes; dashboards no Users a Fabric administrator specifies
Looker Dashboards, LookML dashboards, Looks, Explores, self-service Explores[43] Yes; no badge on signed embedded content since Looker 26.4 Admins or holders of certify_content

“Certified content” here means a badge where the tool has one, otherwise content in a certified project or built only on certified models or data sources.

Automation that clearly helps: CI validation; the change gate (Lab 2); usage-based archiving with owner notice (GitLab’s script; Looker’s scheduled cleanups);[23][37] warnings when a refresh fails.[33] Each needs an owner and a soft-delete path.

AI assistants are a new kind of consumer; writing SQL against raw tables, they inherit every ambiguity. In a 2023 benchmark, GPT-4 answered enterprise questions over an SQL schema with 16% accuracy, and 54% over a knowledge-graph representation of the same database:[44] a direction, not a planning number (one benchmark, domain and model; authors from data.world, a catalogue vendor built on a knowledge graph;[45] its context layer was an ontology with mappings, not a semantic layer, so in this page’s judgement the gain may not carry over). Cube’s agents query its semantic layer, not the warehouse.[24] Snowflake semantic views work in Cortex Agents, but the agent “generates the SQL against the physical tables directly”,[14] so policies on those tables, not the view, decide what it can return. This page’s rules: tier-1 answers use certified metrics and show the definition and version used; queries run with the asking user’s permissions; answers are logged. Operating the assistant belongs to LLMOps.

Assumptions (illustrative): dbt on a cloud warehouse, one main BI tool, many spreadsheets; finance owns revenue metrics. Each definition, its card and its consumers live in one repository, and CI gates changes.

analytics/
  models/marts/fct_orders.sql
  models/marts/fct_orders.yml      # semantic model + certified metrics (Lab 2)
  models/exposures.yml             # dashboards that depend on metrics
  metrics/cards/net_revenue.md     # definition card in business language
  ci/metric_change_gate.py         # Lab 2
  ci/metric_delta.py               # size of a change (Lab 2)
  ci/profiles.yml                  # CI-only profile: local DuckDB file, no warehouse
  ops/content_review.py            # Lab 3
  CHANGELOG-metrics.md             # human-readable change notices

The CI workflow parses the project, runs MetricFlow validations without a warehouse (--skip-dw, a dbt v1 option[28]) and runs the change gate against the base branch, with actions pinned to commit SHAs. Both tools find the CI-only profile through DBT_PROFILES_DIR: mf has no --profiles-dir flag, otherwise looks only in the current directory, and does not read dbt’s newer DBT_ENGINE_PROFILES_DIR.[46]

Example status: workflow and profile YAML syntax-checked; the gate step’s shell logic run in a local Git simulation (new file, changed file, missing base branch), 2026-10-17; neither executed (needs a dbt project and GitHub Actions). Versions checked on PyPI 2026-10-17: dbt-core 1.12.6 (Python 3.10+), dbt-metricflow 0.15.0 (dbt-core 1.11–1.12), dbt-duckdb 1.11.0, PyYAML 6.0.3.

name: metric-ci
on:
  pull_request:  # no paths filter: a required check skipped by a filter stays "Pending"
permissions:
  contents: read
jobs:
  validate-metrics:
    runs-on: ubuntu-24.04
    env:
      DBT_PROFILES_DIR: ci  # read by dbt and mf (mf has no --profiles-dir flag)
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # v7.0.1
        with:
          fetch-depth: 0
          persist-credentials: false  # keep the job token out of .git/config
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97  # v7.0.0
        with:
          python-version: "3.12"
      - name: Install pinned tooling
        run: >-
          python -m pip install "dbt-core==1.12.6" "dbt-duckdb==1.11.0"
          "dbt-metricflow==0.15.0" "pyyaml==6.0.3"
      - name: Parse project (writes semantic_manifest.json)
        run: dbt parse
      - name: Semantic validation without a warehouse
        run: mf validate-configs --skip-dw
      - name: Metric change gate against the base branch
        env:
          BASE_REF: ${{ github.base_ref }}
          FILE: models/marts/fct_orders.yml
        run: |
          # no base branch: this line fails the step (the default shell is bash -e)
          git rev-parse --verify --quiet "origin/${BASE_REF}" > /dev/null
          if git cat-file -e "origin/${BASE_REF}:${FILE}" 2> /dev/null; then
            git show "origin/${BASE_REF}:${FILE}" > "${RUNNER_TEMP}/base.yml"
            python ci/metric_change_gate.py "${RUNNER_TEMP}/base.yml" "${FILE}"
          else
            echo "No ${FILE} on ${BASE_REF}: new file, gate skipped"
          fi
# ci/profiles.yml: the top-level key must match `profile:` in dbt_project.yml
analytics:
  target: ci
  outputs:
    ci:
      type: duckdb
      path: ci.duckdb  # local file: no warehouse, no credentials

Who can reach the token. It is read-only (contents: read), and persist-credentials: false keeps it out of .git/config, where actions/checkout stores it by default,[47] so the packages that later steps install cannot read it there. The base branch reaches the script through an environment variable, against script injection. No paths filter: GitHub leaves checks of a workflow skipped by a path filter “Pending”, which blocks pull requests that require them.[48]

Lab 1 (beginner): find conflicting definitions in your dashboards

Link to section “Lab 1 (beginner): find conflicting definitions in your dashboards”

Objective: find names with several definitions, and definitions under several names. Prerequisites: Python 3, standard library only (use a supported release; tested with 3.8.10); an inventory of tiles on your 10–20 most-viewed dashboards (name, aggregation, measure, source, filters, time zone). No cost or risk; about two hours.

  1. Save the inventory below as metric-inventory.csv (illustrative data; filters joined with &) and the script as metric_conflicts.py.
  2. Run python3 metric_conflicts.py metric-inventory.csv; echo $?.
  3. Replace the sample with your own inventory and run again.
dashboard,metric_name,agg,measure,source,filters,time_zone,owner
Exec,Revenue,sum,amount,fct_orders,status=completed,UTC,fin-bi
Pipeline,Revenue,sum,amount,fct_orders,status in completed+refunded,UTC,sales-ops
Marketing,Net Revenue,sum,amount_net,fct_orders,status=completed&is_test=0,UTC,mkt
Finance,net_revenue,sum,amount_net,fct_orders,is_test=0&status=completed,UTC,fin-bi
Exec,Active customers,count_distinct,customer_id,fct_orders,ordered_in=30d,UTC,fin-bi
Product,Active Customers,count_distinct,customer_id,fct_sessions,seen_in=28d,UTC,prod-an
Product,MAU,count_distinct,user_id,fct_sessions,seen_in=30d,UTC,prod-an
Growth,Monthly active users,count_distinct,user_id,fct_sessions,seen_in=30d,UTC,growth
Support,Tickets opened,count,ticket_id,fct_tickets,,Europe/Berlin,support-ops
Exec,Tickets opened,count,ticket_id,fct_tickets,,UTC,fin-bi

Example status: verified by execution (Python 3.8.10, 2026-10-17); the output below is the actual output.

"""Find conflicting metric definitions in a dashboard inventory.

Usage: python3 metric_conflicts.py metric-inventory.csv
Standard library only. Reports (1) one name with several definitions and
(2) one definition published under several names. Exit code 1 on (1).
"""
import csv
import re
import sys
from collections import defaultdict


def norm_name(name):
    return re.sub(r"[\s_\-]+", " ", name.strip().lower())


def signature(row):
    filters = sorted(f.strip() for f in row["filters"].split("&") if f.strip())
    parts = [row["agg"], row["measure"], row["source"],
             "&".join(filters) or "-", row["time_zone"]]
    return " | ".join(p.strip().lower() for p in parts)


def main(path):
    with open(path, newline="", encoding="utf-8") as fh:
        rows = list(csv.DictReader(fh))
    by_name = defaultdict(lambda: defaultdict(list))
    by_sig = defaultdict(set)
    for r in rows:
        sig = signature(r)
        where = f"{r['dashboard']} ({r['owner']})"
        by_name[norm_name(r["metric_name"])][sig].append(where)
        by_sig[sig].add(norm_name(r["metric_name"]))
    conflicts = {n: s for n, s in by_name.items() if len(s) > 1}
    synonyms = {s: n for s, n in by_sig.items() if len(n) > 1}
    print(f"{len(rows)} tiles, {len(by_name)} metric names, {len(by_sig)} definitions")
    print(f"SAME NAME, DIFFERENT DEFINITIONS: {len(conflicts)}")
    for name, sigs in sorted(conflicts.items()):
        print(f"  '{name}'")
        for sig, places in sigs.items():
            print(f"    {sig}\n      used in: {'; '.join(places)}")
    print(f"SAME DEFINITION, DIFFERENT NAMES: {len(synonyms)}")
    for sig, names in sorted(synonyms.items()):
        print(f"  {' = '.join(sorted(names))}\n    {sig}")
    return 1 if conflicts else 0


if __name__ == "__main__":
    if len(sys.argv) != 2:
        sys.exit("usage: python3 metric_conflicts.py <inventory.csv>")
    sys.exit(main(sys.argv[1]))
$ python3 metric_conflicts.py metric-inventory.csv; echo $?
10 tiles, 6 metric names, 8 definitions
SAME NAME, DIFFERENT DEFINITIONS: 3
  'active customers'
    count_distinct | customer_id | fct_orders | ordered_in=30d | utc
      used in: Exec (fin-bi)
    count_distinct | customer_id | fct_sessions | seen_in=28d | utc
      used in: Product (prod-an)
  'revenue'
    sum | amount | fct_orders | status=completed | utc
      used in: Exec (fin-bi)
    sum | amount | fct_orders | status in completed+refunded | utc
      used in: Pipeline (sales-ops)
  'tickets opened'
    count | ticket_id | fct_tickets | - | europe/berlin
      used in: Support (support-ops)
    count | ticket_id | fct_tickets | - | utc
      used in: Exec (fin-bi)
SAME DEFINITION, DIFFERENT NAMES: 1
  mau = monthly active users
    count_distinct | user_id | fct_sessions | seen_in=30d | utc
1

Expected output: as above; exit code 1. “Net Revenue” and “net_revenue” are not flagged: names are normalised and filter order ignored. Validation: each conflict ends as one canonical metric or two qualified names (“Revenue (bookings, incl. refunded)”). Troubleshooting: KeyError means a misspelt header; quote fields that contain commas. Teardown: delete the files, or keep the inventory as your first catalogue.

Lab 2 (intermediate): a governed metric and a change gate

Link to section “Lab 2 (intermediate): a governed metric and a change gate”

Objective: define certified metrics in dbt’s latest YAML spec with a dependent dashboard; block breaking changes, model-level ones included, that lack a version bump and change note; measure a change. Prerequisites: Python 3 with PyYAML; optionally a dbt v1.12+ project (Python 3.10+) and a warehouse. No cost or risk; about an hour.

  1. Save the YAML below as base.yml and the gate as metric_change_gate.py.
  2. Copy base.yml to head.yml; in head.yml change the gross_revenue filter to {{ Dimension('order__status') }} in ('completed', 'partially_refunded') and the refunds label to Refunds (EUR).
  3. Run python3 metric_change_gate.py base.yml head.yml; echo $? (expected: blocked).
  4. Copy head.yml to head-fixed.yml; there, set gross_revenue’s definition_version to 4 and add change_note: "2026-11-01: include partially refunded orders (FIN-2041)"; run the gate on it (expected: ok).
  5. Save base.yml without the top-level refund_rate metric as head-removed.yml; run the gate (expected: blocked).
  6. Save base.yml as head-time.yml with the model’s agg_time_dimension set to shipped_at and a shipped_at time column added; run the gate (expected: blocked). Adding a dimension alone passes.
  7. Optional, in a real dbt project: dbt parse, then mf validate-configs --skip-dw (self-hosted) or dbt sl validate.[30]

Example status: YAML parsed with PyYAML; written to the dbt v1.12+ spec as documented;[16][17] not validated with dbt or MetricFlow (not installed in the test environment).

models:
  - name: fct_orders
    description: One row per order; refunds netted per order.
    semantic_model:
      enabled: true
      name: orders
    agg_time_dimension: ordered_at
    columns:
      - name: order_id
        entity:
          type: primary
          name: order
      - name: customer_id
        entity:
          type: foreign
          name: customer
      - name: ordered_at
        granularity: day
        dimension:
          type: time
      - name: status
        dimension:
          type: categorical
      - name: is_test
        dimension:
          type: categorical
    metrics:
      - name: gross_revenue
        label: Gross revenue
        description: >
          Sum of order amounts (EUR, excl. VAT) for completed, non-test orders,
          dated by order date in UTC.
        type: simple
        agg: sum
        expr: amount_eur_net_of_vat
        filter: |
          {{ Dimension('order__status') }} = 'completed'
          and {{ Dimension('order__is_test') }} = false
        config:
          meta:
            owner: finance-bi
            tier: 1
            status: certified
            definition_version: 3
      - name: refunds
        label: Refunds
        description: Refunded amount (EUR, excl. VAT) by order date.
        type: simple
        agg: sum
        expr: refund_amount_eur
        config:
          meta:
            owner: finance-bi
            tier: 1
            status: certified
            definition_version: 1
      - name: net_revenue
        label: Net revenue
        description: Gross revenue minus refunds.
        type: derived
        expr: gross_revenue - refunds
        config:
          meta:
            owner: finance-bi
            tier: 1
            status: certified
            definition_version: 2

metrics:
  - name: refund_rate
    label: Refund rate
    description: Refunds as a share of gross revenue.
    type: ratio
    numerator: refunds
    denominator: gross_revenue
    config:
      meta:
        owner: finance-bi
        tier: 2
        status: certified
        definition_version: 1

exposures:
  - name: exec_weekly_revenue
    label: Executive weekly revenue
    type: dashboard
    maturity: high
    url: https://bi.example.com/dashboards/42
    depends_on:
      - metric('net_revenue')
      - metric('refund_rate')
    owner:
      name: Finance BI
      email: finance-bi@example.com

The config.meta keys are this page’s convention; meta “accepts any key-value pairs”.[49] Exposures usually live in their own file.

Example status: verified by execution (Python 3.8.10, PyYAML 5.3.1, 2026-10-17); the outputs below are actual. The reviewer’s primary-entity change (order_id to order_line_id) is also blocked.

"""Gate metric-definition changes in a pull request (dbt latest YAML spec).

Usage: python3 metric_change_gate.py base.yml head.yml
Needs PyYAML. A changed definition must raise meta.definition_version and add
meta.change_note; a removed metric must have been deprecated first; a new
metric needs owner, tier and status. A changed or removed model-level key
(agg_time_dimension, semantic model, entity or dimension) redefines every
metric in that model. Prints the metrics and dashboards (exposures) affected.
Exit code 1 on any violation. It does not see model SQL or upstream tables.
"""
import re
import sys

import yaml

DEFINITION_KEYS = ("type", "agg", "expr", "filter", "numerator", "denominator",
                   "input_metric", "input_metrics", "window", "grain_to_date",
                   "agg_time_dimension", "non_additive_dimension", "fill_nulls_with",
                   "join_to_timespine", "base_metric", "conversion_metric")
REQUIRED_META = ("owner", "tier", "status")


def model_keys(model):
    """Model-level semantics: a change or removal here alters every metric."""
    keys = {"agg_time_dimension": model.get("agg_time_dimension"),
            "semantic_model": model.get("semantic_model")}
    for col in model.get("columns") or []:
        for kind in ("entity", "dimension", "granularity"):
            if col.get(kind) is not None:
                keys[f"{col['name']}.{kind}"] = col[kind]
    return keys


def load(path):
    with open(path, encoding="utf-8") as fh:
        doc = yaml.safe_load(fh) or {}
    metrics, models = {}, {}
    for model in doc.get("models") or []:
        models[model["name"]] = model_keys(model)
        for m in model.get("metrics") or []:
            metrics[m["name"]] = dict(m, _model=model["name"])
    for m in doc.get("metrics") or []:
        metrics[m["name"]] = dict(m, _model="(top level)")
    exposures = {e["name"]: [re.sub(r"^metric\('(.+)'\)$", r"\1", d)
                             for d in e.get("depends_on") or []]
                 for e in doc.get("exposures") or []}
    return metrics, models, exposures


def meta(metric):
    return (metric.get("config") or {}).get("meta") or {}


def definition(metric):
    return {k: metric.get(k) for k in DEFINITION_KEYS + ("_model",)}


def inputs(metric):
    found = set()
    for key in ("numerator", "denominator", "input_metric", "base_metric",
                "conversion_metric"):
        val = metric.get(key)
        if val:
            found.add(val["name"] if isinstance(val, dict) else val)
    for item in metric.get("input_metrics") or []:
        found.add(item["name"])
    if metric.get("type") == "derived":
        found |= set(re.findall(r"[a-z_][a-z0-9_]*", metric.get("expr", "")))
    return found


def downstream(changed, metrics, exposures):
    affected, frontier = set(changed), set(changed)
    while frontier:
        frontier = {n for n, m in metrics.items()
                    if inputs(m) & frontier and n not in affected}
        affected |= frontier
    dashboards = sorted(e for e, deps in exposures.items() if set(deps) & affected)
    return sorted(affected - set(changed)), dashboards


def changed_models(base_models, head_models):
    """Models whose existing semantic keys changed or vanished (additions pass)."""
    hit = set()
    for name, old in base_models.items():
        new = head_models.get(name, {})
        diff = sorted(k for k in old if old[k] != new.get(k))
        if diff:
            print(f"MODEL     {name}: {', '.join(diff)} changed or removed")
            hit.add(name)
    return hit


def main(base_path, head_path):
    base, base_models, _ = load(base_path)
    head, head_models, exposures = load(head_path)
    models_hit = changed_models(base_models, head_models)
    problems, changed = [], []
    for name in sorted(set(base) | set(head)):
        old, new = base.get(name), head.get(name)
        if old is None:
            missing = [k for k in REQUIRED_META if k not in meta(new)]
            print(f"ADDED     {name}")
            if missing:
                problems.append(f"{name}: new metric lacks meta {missing}")
        elif new is None:
            print(f"REMOVED   {name}")
            changed.append(name)
            if meta(old).get("status") != "deprecated":
                problems.append(f"{name}: removed without being deprecated first")
        elif definition(old) != definition(new) or old["_model"] in models_hit:
            print(f"REDEFINED {name}")
            changed.append(name)
            v_old = int(meta(old).get("definition_version", 0))
            v_new = int(meta(new).get("definition_version", 0))
            if v_new <= v_old or not meta(new).get("change_note"):
                problems.append(f"{name}: definition changed: bump definition_version"
                                f" (now {v_new}), add change_note")
        elif old != new:
            print(f"COSMETIC  {name} (label, description or meta only)")
    if changed:
        metrics_hit, dashboards = downstream(changed, head, exposures)
        print(f"Affected metrics: {', '.join(metrics_hit) or 'none'}")
        print(f"Affected dashboards: {', '.join(dashboards) or 'none'}")
    for p in problems:
        print(f"FAIL {p}")
    print("RESULT:", "blocked" if problems else "ok")
    return 1 if problems else 0


if __name__ == "__main__":
    if len(sys.argv) != 3:
        sys.exit("usage: python3 metric_change_gate.py <base.yml> <head.yml>")
    sys.exit(main(sys.argv[1], sys.argv[2]))
$ python3 metric_change_gate.py base.yml head.yml; echo $?
REDEFINED gross_revenue
COSMETIC  refunds (label, description or meta only)
Affected metrics: net_revenue, refund_rate
Affected dashboards: exec_weekly_revenue
FAIL gross_revenue: definition changed: bump definition_version (now 3), add change_note
RESULT: blocked
1
$ python3 metric_change_gate.py base.yml head-fixed.yml; echo $?
REDEFINED gross_revenue
COSMETIC  refunds (label, description or meta only)
Affected metrics: net_revenue, refund_rate
Affected dashboards: exec_weekly_revenue
RESULT: ok
0
$ python3 metric_change_gate.py base.yml head-removed.yml; echo $?
REMOVED   refund_rate
Affected metrics: none
Affected dashboards: exec_weekly_revenue
FAIL refund_rate: removed without being deprecated first
RESULT: blocked
1
$ python3 metric_change_gate.py base.yml head-time.yml; echo $?
MODEL     fct_orders: agg_time_dimension changed or removed
REDEFINED gross_revenue
REDEFINED net_revenue
REDEFINED refunds
Affected metrics: refund_rate
Affected dashboards: exec_weekly_revenue
FAIL gross_revenue: definition changed: bump definition_version (now 3), add change_note
FAIL net_revenue: definition changed: bump definition_version (now 2), add change_note
FAIL refunds: definition changed: bump definition_version (now 1), add change_note
RESULT: blocked
1

Expected output: as above. A one-word filter change to gross_revenue also changes net_revenue, refund_rate and the executive dashboard; a new default time dimension changes every metric of the model. Only redefined metrics need a version bump, because definition_version versions a metric’s own definition (this page’s convention); dependants’ values still change, so the notice names them and their cards’ change logs cite gross_revenue v4. Validation: the impact list names every dependent exposure; their owners get the notice. Troubleshooting: No module named 'yaml': install PyYAML; KeyError: 'name': a metric or exposure lacks a name. Teardown: delete the files. Limits: the gate reads one file, sees only consumers declared as exposures, and not the model’s SQL or upstream tables, which DataOps tests.

Measure the size of the change. The notice needs a number: run the affected metrics under both definitions on the same data and window, then compare.

Example status: the mf query commands follow the documented syntax (--csv exists only in self-hosted mf, not in dbt sl)[28] and were not executed (no warehouse); metric_delta.py was verified by execution (Python 3.8.10, 2026-10-17) on the illustrative CSV files shown.

git switch main && dbt parse
mf query --metrics gross_revenue,net_revenue --group-by metric_time__month \
  --start-time 2026-07-01 --end-time 2026-09-30 --csv base.csv
git switch fin-2041 && dbt parse
mf query --metrics gross_revenue,net_revenue --group-by metric_time__month \
  --start-time 2026-07-01 --end-time 2026-09-30 --csv head.csv
python3 metric_delta.py base.csv head.csv
base.csv
metric_time__month,gross_revenue,net_revenue
2026-07-01,1204000.00,1150300.00
2026-08-01,1187500.00,1131900.00
2026-09-01,1250200.00,1192800.00

head.csv
metric_time__month,gross_revenue,net_revenue
2026-07-01,1228100.00,1174400.00
2026-08-01,1214800.00,1159200.00
2026-09-01,1282700.00,1225300.00
"""Size of a metric-definition change: compare base and head query results.

Usage: python3 metric_delta.py base.csv head.csv
Standard library only. Each CSV has the period in its first column and one
column per metric (for example from `mf query ... --csv`), run on the same
data and window. Prints each metric's total change and its largest period change.
"""
import csv
import sys


def read(path):
    with open(path, newline="", encoding="utf-8") as fh:
        rows = list(csv.reader(fh))
    header = [h.strip().lower() for h in rows[0]]
    return {r[0]: dict(zip(header[1:], map(float, r[1:]))) for r in rows[1:]}, header[1:]


def pct(old, new):
    return (new - old) / old * 100 if old else float("nan")


def main(base_path, head_path):
    base, metrics = read(base_path)
    head, _ = read(head_path)
    periods = sorted(set(base) & set(head))
    print(f"{len(periods)} periods compared: {periods[0]} to {periods[-1]}")
    for m in metrics:
        old = sum(base[p][m] for p in periods)
        new = sum(head[p][m] for p in periods)
        worst = max(periods, key=lambda p: abs(pct(base[p][m], head[p][m])))
        print(f"{m:<14} {old:>14,.2f} -> {new:>14,.2f}  {pct(old, new):+.1f}%"
              f"  (largest: {worst} {pct(base[worst][m], head[worst][m]):+.1f}%)")
    return 0


if __name__ == "__main__":
    if len(sys.argv) != 3:
        sys.exit("usage: python3 metric_delta.py <base.csv> <head.csv>")
    sys.exit(main(sys.argv[1], sys.argv[2]))
$ python3 metric_delta.py base.csv head.csv
3 periods compared: 2026-07-01 to 2026-09-01
gross_revenue    3,641,700.00 ->   3,725,600.00  +2.3%  (largest: 2026-09-01 +2.6%)
net_revenue      3,475,000.00 ->   3,558,900.00  +2.4%  (largest: 2026-09-01 +2.7%)

Put the total (+2.3%) on the notice’s Size line; the head run’s monthly values are the back series. Both runs must read the same snapshot, or a late partition looks like a change. For tier-1 values also keep tests that run on every change: a LookML test whose assert checks a known historical value,[50] or a reconciliation with the ledger for a closed month.

Lab 3 (advanced): a usage-based content review

Link to section “Lab 3 (advanced): a usage-based content review”

Objective: a quarterly worklist of stale, orphaned and certified-but-unused content, plus the active-content ratio. Prerequisites: Python 3, standard library only; 90 days of usage data plus each item’s active deliveries (retention and counting rules: tooling landscape). Risk: archiving is visible; notify first and soft-delete only. Status: script executed; archiving steps not executed.

  1. Export one row per item: owner, owner still active, certified, created, last viewed, viewers in 90 days, active deliveries (schedules, subscriptions, embeds or API pulls you can count), keep flag (GitLab uses a “Do NOT Archive” tag).[23]
  2. Run the review with the export date.
  3. Notify owners (14 days to object); archive by soft delete; reassign orphans; send certified-but-unused items to the certifier.
  4. Record the ratio for the trend.
item,owner,owner_active,certified,created,last_viewed,viewers_90d,active_deliveries,keep
Exec weekly revenue,fin-bi,Y,Y,2024-02-01,2026-10-16,48,0,N
Board pack KPIs,fin-bi,Y,Y,2023-06-12,2026-07-02,0,1,N
Revenue by region (copy),a.lee,N,N,2025-03-03,2026-01-20,0,0,N
Pipeline coverage,sales-ops,Y,Y,2024-09-09,2026-10-15,31,0,N
Pipeline coverage v2,sales-ops,Y,N,2025-11-20,2026-10-10,2,0,N
Churn deep dive,prod-an,Y,N,2025-01-15,2026-04-30,0,0,N
Support backlog,support-ops,Y,Y,2024-05-05,2026-10-17,22,0,N
Marketing ROI,mkt,Y,N,2025-08-01,2026-09-30,6,0,N
Q3 campaign test,j.diaz,N,N,2025-07-14,2025-08-02,0,0,N
Active customers trend,prod-an,Y,Y,2024-03-18,2026-06-01,0,0,N
Year-end close pack,fin-bi,Y,Y,2023-01-10,2026-01-15,0,0,Y
Ops weekly (old),ops,Y,N,2022-10-10,,0,0,N
New pricing monitor,fin-bi,Y,N,2026-09-01,2026-10-12,9,0,N

Example status: verified by execution (Python 3.8.10, 2026-10-17); the output below is the actual output.

"""Quarterly BI content review: find stale, orphaned and unused certified content.

Usage: python3 content_review.py content-usage.csv 2026-10-17 [window_days]
Standard library only. Input: one row per dashboard or report, exported from
the BI tool's usage data. An item counts as used if it was viewed in the window
or has active deliveries (schedules, subscriptions, embeds or API pulls).
Prints the active-content ratio and a worklist.
"""
import csv
import sys
from datetime import date, timedelta


def day(text):
    return date.fromisoformat(text) if text else None


def review(row, as_of, window):
    start = as_of - timedelta(days=window)
    created, seen = day(row["created"]), day(row["last_viewed"])
    viewed = seen is not None and seen >= start
    used = viewed or int(row["active_deliveries"]) > 0
    new = created > start
    actions = []
    if row["owner_active"] != "Y":
        actions.append("reassign owner")
    if new:
        return used, new, actions
    if not used:
        if row["keep"] == "Y":
            actions.append("confirm keep exception")
        elif row["certified"] == "Y":
            actions.append("decertify or retire (certified, unused)")
        else:
            actions.append("archive after 14-day notice")
    elif not viewed:
        actions.append("delivered only: ask recipients if still needed")
    elif row["certified"] != "Y" and int(row["viewers_90d"]) <= 2:
        actions.append("merge or make personal (reach <= 2)")
    return used, new, actions


def main(path, as_of_text, window=90):
    as_of = date.fromisoformat(as_of_text)
    with open(path, newline="", encoding="utf-8") as fh:
        rows = list(csv.DictReader(fh))
    mature = active = 0
    worklist = []
    for r in rows:
        used, new, actions = review(r, as_of, window)
        if not new:
            mature += 1
            active += used
        if actions:
            worklist.append((r["item"], r["owner"], "; ".join(actions)))
    print(f"{len(rows)} items; {mature} older than {window} days; {active} of them"
          f" used (viewed or delivered) in the last {window} days")
    print(f"Active-content ratio: {active / mature:.0%}")
    print("WORKLIST")
    for item, owner, action in worklist:
        print(f"  {item:<24} {owner:<11} {action}")
    return 0


if __name__ == "__main__":
    if len(sys.argv) not in (3, 4):
        sys.exit("usage: python3 content_review.py <usage.csv> <YYYY-MM-DD> [days]")
    sys.exit(main(sys.argv[1], sys.argv[2], *(int(a) for a in sys.argv[3:])))
$ python3 content_review.py content-usage.csv 2026-10-17
13 items; 12 older than 90 days; 6 of them used (viewed or delivered) in the last 90 days
Active-content ratio: 50%
WORKLIST
  Board pack KPIs          fin-bi      delivered only: ask recipients if still needed
  Revenue by region (copy) a.lee       reassign owner; archive after 14-day notice
  Pipeline coverage v2     sales-ops   merge or make personal (reach <= 2)
  Churn deep dive          prod-an     archive after 14-day notice
  Q3 campaign test         j.diaz      reassign owner; archive after 14-day notice
  Active customers trend   prod-an     decertify or retire (certified, unused)
  Year-end close pack      fin-bi      confirm keep exception
  Ops weekly (old)         ops         archive after 14-day notice

Expected output: as above. “New pricing monitor” is younger than the window; “Board pack KPIs” is e-mailed, not opened, so it counts as used and its owner asks the recipients. Validation: with a 180-day window (add 180 as a third argument) the ratio rises to 67% (8 of 12), so always report the window. Troubleshooting: Invalid isoformat string means a date is not YYYY-MM-DD; a ratio that jumps between quarters often means lost export days. Teardown: delete the files; restore late objections from the archive.

All three come from the organisations’ own publications, without independent measurement of outcomes.

Case 1: standardising metric definitions across teams (Uber, sourced)

Link to section “Case 1: standardising metric definitions across teams (Uber, sourced)”

Problem and context. Across “30+ teams and 10K+ metrics”, “completed trips” existed as Presto/Hive SQL for an operations team and as a Cassandra-based metric for Pricing Engineering.[6] Method. uMetric separates the metric definition from dimensions and filters supplied at query time, so “Completed Trips In New York” is not a separate metric; heuristic SQL canonicalisation detects duplicates. Trade-offs. A platform team and onboarding effort; Uber described itself as “still at the early stages of the journey”. Success criteria. New metrics unambiguous and not duplicated. Pitfall targeted: per-filter copies whose names “do not capture the complete details of the dimensions and filters”.[6]

Case 2: one metric platform plus certification (Airbnb, sourced)

Link to section “Case 2: one metric platform plus certification (Airbnb, sourced)”

Problem. Diverging answers from Data Science and Finance. Method. Minerva held “more than 12,000 metrics and 4,000 dimensions” (2021) in a central GitHub repository, serving dashboards, experiments and notebooks from the same definitions, with “certification status, ownership, and popularity” in the catalogue.[7] Midas certification adds design specs, validation and four reviews, one of them of the metric definitions.[11] Trade-offs. Building to Midas standards “is much slower than building uncertified data”.[11] Success criteria. One answer per question in every tool. Pitfall. Certification becomes a bottleneck if applied to everything, which is why Midas reserves it for important company data.

Case 3: retiring stale dashboards on a schedule (GitLab, sourced)

Link to section “Case 3: retiring stale dashboards on a schedule (GitLab, sourced)”

Problem. Clutter on GitLab’s Tableau Cloud site. Method. A quarterly, manually started script moves workbooks not “created, updated, or viewed in the last 90 days” and unused data sources to a restricted Archive folder; a “Do NOT Archive” tag exempts content.[23] Trade-offs. Archived items vanish from search; restores go through the BI team. Success criteria. Only used content on the main site. Pitfall. A 90-day rule catches annual reports (hence the tag), and a view-based rule misses content consumed by e-mail; Lab 3 handles both.

KPI Definition / formula Data source Pitfall Cadence
Governed coverage (tier 1) Tier-1 dashboard tiles on a certified metric ÷ all tier-1 tiles Semantic-layer logs, BI metadata Counting dashboards, not tiles Monthly
Definition conflicts Metric names with more than one active definition (Lab 1) Inventory, catalogue Only checking certified content Monthly
Certified view share Views of certified content (as defined) ÷ all views, 90 days; Tableau Blueprint compares certified and ad hoc use[41] Usage logs, catalogue Rewards over-certification Quarterly
Active-content ratio Items older than N days viewed or delivered in the last N days ÷ items older than N days (Lab 3) Usage and delivery logs Window choice changes the number Quarterly
Freshness attainment Dashboard-days with data by the promised time ÷ dashboard-days promised Refresh logs Ignoring failed refreshes Weekly
Opening time (tier 1) Median and, where logs allow, 95th-percentile time to open each tier-1 dashboard, against a target (Power BI reports the median[34]) BI performance logs, query history Averages hide Monday peaks Monthly
Time to reconcile Median hours from a conflicting-number report to a published explanation Incident log Closing without an explanation Monthly
Unannounced breaking changes Breaking changes released without notice Change gate, change log Missing BI-tool edits outside Git Monthly (target 0)
Cost per active item (BI licences + refresh and query compute) ÷ active items Billing, usage Licences that cannot be cut Quarterly

Leading: conflicts, coverage, unannounced changes. Lagging: time to reconcile, active-content ratio.

Of 1,240 dashboards and reports, 1,180 are older than 90 days and 410 of those were viewed or delivered in the last 90 days: active-content ratio = 410 ÷ 1,180 = 34.7%; two-thirds of the mature estate is refreshed and secured but unused. The 24 tier-1 dashboards have 210 tiles, 172 on certified metrics: governed coverage = 172 ÷ 210 = 81.9%; the other 38 tiles are where the next conflict comes from. They promise data by 07:00 UTC; 698 of 720 dashboard-days in a 30-day month met it: freshness attainment = 96.9% against a 98% target. Of 95,000 views, 61,750 were of certified content: certified view share = 65%. Next quarter: fix the 38 tiles, archive the 770 stale items after notice, take the 22 late days to DataOps.

Vanity traps: counts of dashboards or metrics; logins; “100% certified”, which means the badge has lost its meaning.

Risk Signal Mitigation Owner
Defined one way, calculated another Card and code disagree One pull request for both; definitional tests; tier-1 reconciliation Analytics engineer
Certified content bypassed Warehouse queries re-create a certified metric Make the certified path easiest; Lab 1 Analytics lead
Policy bypass via exports or direct connections Large exports; unmanaged extracts Access enforced in warehouse or semantic layer;[24] exports limited (DataSecOps) Platform admin
Small groups identifiable in aggregates Counts of a few people Minimum group size (PrivacyOps) Metric owner
Assistant answers outside policy Uncertified paths; service-account rights User-scoped queries; logging Analytics lead
No evidence of who changed a regulated metric Edits in the BI tool, outside Git Pull requests; keep logs beyond platform defaults (retention) Platform admin

Quality means code that matches its card and a number that reconciles with its system of record; Midas requires automated checks including “definitional testing”.[11] Compliance: where a metric feeds financial statements or regulatory returns, its change control is likely audited; agree the evidence with your controls team (ComplianceOps). General guidance, not legal advice.

Cost drivers: refresh frequency times model size; dashboard queries on the warehouse; licences; duplicated models. Power BI allows eight scheduled refreshes a day on shared capacity, up to 48 on Premium, PPU or Fabric capacity.[51] Unused content still costs; Power BI pausing a semantic model’s refresh after two months without report visits is a backstop, not a policy.[51] Minerva built in “cost attribution” and “usage-based retention and garbage collection”.[7] Performance. Slow tier-1 dashboards are an operating problem (opening-time KPI), worst when everyone opens them on Monday morning. Pre-aggregations and extracts are the main lever; Cube refreshes pre-aggregations in the background,[24] Power BI discards cached results after each refresh,[51] and both serve data as old as the last refresh, so show that age. Query tuning belongs to DBOps, latency monitoring to ObservabilityOps. Sustainability: fewer refreshes of unused content means less compute (GreenOps); route spend through FinOps.

  • Quarterly sample audit: five certified metrics, each with a current card matching the code, an owner, tests and consumers.
  • Risk indicators (this page’s thresholds): an ownerless tier-1 metric; any tier-1 conflict; an unannounced breaking change; active ratio falling two quarters running; freshness below target; usage or delivery data unexported for over a week.
Symptom Likely cause and diagnosis Remediation Prevention
Two dashboards, two numbers Different definitions (filter, grain, time zone, currency, refunds); compare cards or SQL Name a canonical metric; rename or retire the other Duplicate check at intake; Lab 1
A number jumps after a release, no data change Silent definition change; check Git history Revert, or publish with size and back series Change gate
Tiles break after a model change Renamed or removed fields; Looker’s Content Validator finds them, with no undo[31] Repair references with the release Impact list from exposures[19]
Yesterday’s data shown as current Failed or paused refresh; check refresh history[51] Re-run; warn on the content[33] Freshness KPI; “data as of” label
Certified dashboard nobody uses Need moved on; certification never reviewed[37] Decertify or retire after notice Quarterly content review

A sourced failure: defined one way, calculated another. In 2016 Facebook disclosed on its advertiser help page, as quoted by TechCrunch, that it had defined Average Duration of Video Viewed as “total time spent watching a video divided by the total number of people who have played the video” but calculated it over “only the number of people who have viewed a video for three or more seconds”. Publicis Media told The Wall Street Journal the inflation was 60 to 80 percent, TechCrunch reported; Facebook said billing was unaffected.[52] In 2019 a proposed $40 million settlement acknowledged calculation errors without admitting other allegations, according to Marketing Dive.[53] A written definition is a control only if something checks the calculation.

Worked runbook: two numbers for one metric

Link to section “Worked runbook: two numbers for one metric”

Illustrative scenario. At Monday’s executive review, “Active customers” shows 48,200 on the executive dashboard and 51,900 on the product dashboard. The CEO asks which is right.

Figure 5. Triage for conflicting numbers: definition, data or calculation
NoYesNoYesTwo numbers for one metricLabel both: under reviewSame definition card?Definition conflictOwner picks canonical;rename the otherSame data snapshot?Freshness or data incidentHand to DataOps runbookCalculation or tool faultFix filter, time zone, cache oraggregationPublish explanation; addprevention
Diagram source (Mermaid)
flowchart TB
  A["Two numbers for one metric"] --> L["Label both: under review"]
  L --> Q1{"Same definition card?"}
  Q1 -- "No" --> D1["Definition conflict"]
  D1 --> F1["Owner picks canonical; rename the other"]
  Q1 -- "Yes" --> Q2{"Same data snapshot?"}
  Q2 -- "No" --> D2["Freshness or data incident"]
  D2 --> F2["Hand to DataOps runbook"]
  Q2 -- "Yes" --> D3["Calculation or tool fault"]
  D3 --> F3["Fix filter, time zone, cache or aggregation"]
  F1 --> P["Publish explanation; add prevention"]
  F2 --> P
  F3 --> P
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef risk fill:#fbeceb,stroke:#b4332b,color:#122029
  class A,L,F1,F2,F3,P core
  class Q1,Q2 decision
  class D1,D2,D3 risk

Text equivalent: Label both numbers as under review. Different definition cards: a definition conflict; the owner picks the canonical metric and the other is renamed. Same cards, different data snapshots: a freshness or data incident for DataOps. Both match: a calculation or tool fault (filter, time zone, cache, aggregation). Every branch ends with a published explanation and a prevention step.

  1. Label (first 30 minutes). A banner on both dashboards so nobody forwards either number; say when an answer is due.
  2. Compare definitions (two hours). Both cards, or the queries. Here the executive dashboard counts customers with an order in the last 30 days; the product dashboard, customers with a session in the last 28 days: a definition conflict, not an error.
  3. Decide. The metric owner (finance, as the board uses it) keeps “Active customers” for the order-based definition; product’s becomes “Engaged customers (sessions, 28-day)”. Unclear ownership goes to the metrics council that day.
  4. If the definitions had matched: compare data snapshots (a difference goes to the DataOps incident process), then the calculation: filters, time zones, caches, row-level security and aggregation (daily distinct counts summed into a month, or an average of ratios instead of a ratio of totals). A stale filter in one copy, Uber’s case, lands here.
  5. Communicate (same day): both numbers and definitions, the canonical one, what changes where and when.
  6. Prevent (two weeks): catalogue both metrics; add Lab 1 to the monthly review; certify the canonical metric; gate the product team’s models.
Anti-pattern or myth Why it fails Warning signal Do instead
“The semantic layer will fix trust” Tools serve definitions; they do not agree them Layer live, owners unnamed Name owners and write cards first
Certify everything The badge stops meaning anything; reviews become rubber stamps Most content certified Certify tier 1 and 2; review samples
Silent fixes Consumers see a jump and lose trust Changes outside Git Version, notice, back series[22]
Metric as a target without safeguards People optimise the measure, not the outcome[26] Sudden gains with no business change Pair metrics; audit definitions
Retire by hard delete Lost history; angry owners No trash or archive Notice, soft delete, restore path[37]
Retire on views alone E-mailed and embedded content looks unused Board packs on the archive list Count deliveries (Lab 3)

Invest in AnalyticsOps when most of these hold:

  • Several teams or tools report the same KPIs, and meetings stall on “which number is right”.
  • Numbers reach a board, investors, regulators or customers, or drive prices, bonuses or automated decisions.
  • Self-service outgrows review, or AI assistants are about to answer metric questions.

Do not invest yet, or keep it minimal, when:

  • One team, one tool, a few dozen dashboards: a short catalogue and certified tables are enough.
  • The data underneath is unreliable: fix DataOps first, or governance makes wrong numbers consistently wrong.
  • No business owner will own definitions: governance without owners is theatre.

Hidden costs: review time; plan tiers for certification, deployment pipelines or semantic-layer APIs;[42][32][28] notices, measured changes and back series. Exit criteria: with tier-1 conflicts at zero and coverage high, keep only the quarterly review.

First decide whether to adopt a semantic layer at all, then where definitions should live; for dbt users the plan decides (tooling landscape).

Figure 6. Decision tree: where should governed metric definitions live?
NoYesNoYesYesNoYesNoMetrics to governShared by teams ortools?Not yet: catalogueplus certified tablesConsumers beyondone BI tool?BI-native model:LookML or Power BImodeldbt standard, with adbt platform plan?Transformation layer(dbt Semantic Layer)One warehouse is thehub?Warehouse-nativesemanticsStandalone semanticlayerThen: owners, cards,certification, changegate
Diagram source (Mermaid)
%%{init: {"flowchart": {"wrappingWidth": 150, "nodeSpacing": 24, "rankSpacing": 36}}}%%
flowchart TB
  S["Metrics to govern"] --> Q0{"Shared by teams or tools?"}
  Q0 -- "No" --> N["Not yet: catalogue plus certified tables"]
  Q0 -- "Yes" --> Q1{"Consumers beyond one BI tool?"}
  Q1 -- "No" --> B["BI-native model: LookML or Power BI model"]
  Q1 -- "Yes" --> Q2{"dbt standard, with a dbt platform plan?"}
  Q2 -- "Yes" --> T["Transformation layer (dbt Semantic Layer)"]
  Q2 -- "No" --> Q3{"One warehouse is the hub?"}
  Q3 -- "Yes" --> W["Warehouse-native semantics"]
  Q3 -- "No" --> C["Standalone semantic layer"]
  B --> G["Then: owners, cards, certification, change gate"]
  T --> G
  W --> G
  C --> G
  classDef core fill:#e8f3f2,stroke:#16756f,color:#122029
  classDef decision fill:#fdf3e3,stroke:#c9800f,color:#122029
  classDef neutral fill:#f1f4f6,stroke:#5b7a87,color:#122029
  class S,B,T,W,C,G core
  class Q0,Q1,Q2,Q3 decision
  class N neutral

Text equivalent: If one team or tool uses the metrics, do not adopt a semantic layer yet; keep a catalogue and certified tables. If all consumers sit in one BI tool, use its model. Beyond one tool, use the dbt Semantic Layer if dbt is the modelling standard and a dbt platform plan is acceptable; otherwise warehouse-native objects if one warehouse is the hub, else a standalone semantic layer. All options then need owners, cards, certification and a change gate.

When two options are close, score them. Illustrative for a company with dbt, two BI tools, many spreadsheets and a planned AI assistant; scores 1 (poor) to 5 (good); weighted score = Σ(weight × score) ÷ 5, out of 100.

Criterion (weight) BI-native Transformation layer Warehouse-native Standalone
Reach across consumers (30) 2 4 4 5
Fit with current skills (20) 5 5 3 2
Operating burden (20) 5 4 4 2
Lock-in and portability (15) 2 3 2 3
Cost incl. plan tiers (15) 4 3 3 3
Weighted score 70 78 67 64

The transformation layer wins because dbt skills exist and the plan is affordable. Weight reach more (embedded analytics) and the standalone layer, with access rules and caching in one API,[24] catches up; without the assistant and second BI tool, the BI-native model suffices.

These levels are this Atlas’s model for metric governance. For platform adoption, Microsoft’s five levels, based on the Capability Maturity Model (CMM), from 100 Initial to 500 Efficient, advise aiming for “at least maturity level 300 or 400”.[25]

Level Observable behaviour Evidence Exit criteria
0. Numbers by negotiation Conflicts found in meetings; definitions in queries “Which number?” escalations Top dashboards inventoried (Lab 1)
1. Catalogued Top metrics listed with owners Catalogue; conflict list Every tier-1 metric has owner and card
2. Governed core Tier-1 metrics as code; badges; change gate Pull requests, CI logs No tier-1 conflict or unannounced change for a quarter
3. Managed self-service Team content on certified models; usage review; freshness visible Coverage, active ratio Tier-1 coverage ≥ 90%; active ratio rising
4. Metrics as products Every consumer, assistants included, uses the same versioned definitions Incident and query logs Sustain; periodic audit

Level 2 suits most tier-1 metrics, level 3 heavy self-service; assess per domain.

Assumes an analytics lead, a part-time analytics engineer, one BI tool plus spreadsheets, and a finance sponsor.

Window Goals Activities Metrics at the end
Days 1–30: see the problem Know the conflicts Inventory the top 20 dashboards (Lab 1); owners for the top 15 metrics; daily usage and delivery export Conflict count; owners named
Days 31–60: govern the core One definition per tier-1 metric Cards; tier-1 metrics in one semantic layer or BI model; certification criteria; change gate (Lab 2); “data as of” labels Tier-1 coverage; no unannounced changes
Days 61–90: open it up Self-service on certified models First content review (Lab 3); creator training; governance review; runbook rehearsal Active ratio; time to reconcile; freshness

Quick wins: settle the loudest conflicts; archive clearly dead content after notice. Dependencies: reliable tables (DataOps), admin access to usage data, a sponsor who arbitrates definitions.

  • Catalogue searched; no existing metric or qualified variant fits.
  • Owner named, tier assigned, card complete.
  • Encoded with owner, tier, status and version metadata; validation passes.
  • Card and code match, in one pull request; tier 1 reconciled with its system of record.
  • Consumers declared; freshness promise written.
  • Certifier is not the author; notes say what the badge means.
  • Usage and delivery data cover the whole window, with no export gaps.
  • Worklist produced (Lab 3); owners notified; keep exceptions re-confirmed; archive restorable.
  • Active ratio and coverage recorded with the window used.
  • Class recorded (definition, data, calculation); explanation sent to all who saw both numbers.
  • Canonical metric certified; the other renamed or retired; prevention step merged.
METRIC: <name>                  Version: <n>       Status: draft | certified | deprecated
Owner (accountable): <person, team>     Steward (encodes): <person>     Tier: 1 | 2 | 3
Business definition: <one or two sentences a non-analyst can check>
Formula: <aggregation> of <measure> from <source>  Grain: <row / time grain>
Additivity: <e.g. sums across regions, not across days (distinct count)>
Includes: <...>                         Excludes: <...>
Time zone and calendar: <e.g. UTC, ISO weeks>     Currency rule: <e.g. EUR at daily rate>
Late data: <e.g. restated for 3 days, then frozen>
Freshness promise: <e.g. 98% of days by 07:00 UTC>
Reconciles with: <system of record, tolerance>
Known caveats: <...>
Consumers: <dashboards, exports, models, assistants>
Change log: <version, date, change, reason, notice link>
METRIC CHANGE NOTICE — <metric> v<old> -> v<new>        Effective: <YYYY-MM-DD>
What changes: <old definition> -> <new definition>
Why: <business reason or error correction; ticket>
Size: <measured, e.g. +2.3% over Jul-Sep 2026; back series attached>
Affected: <dashboards and owners from the impact list>
Action for you: <none | update a filter | recheck a target>
Contact: <owner>       Objections by: <date>
CONTENT REVIEW — <quarter>           To: <owner>
These items have not been viewed for <N> days (usage data from <tool>, exported <date>):
  <item> | last viewed <date> | proposed action <archive | decertify | reassign>
Reply by <date + 14 days> to keep an item (state why and for how long).
Archived items can be restored from <archive location> until <date>.
Criterion Pass condition
Ownership Named business owner and steward
Definition Card complete and matches code
Correctness Tests pass; tier 1 reconciled
Freshness Promise written; attainment measured
Consumers Declared; impact list possible
Usage Viewed in the last 90 days, an active schedule or subscription, or a documented exception

Is AnalyticsOps just DataOps for dashboards? Partly: it reuses DataOps habits, but its object is the definition people read; perfect data can still produce disagreeing dashboards.

Who should own a metric definition: the BI team or the business? The business owns meaning and approves changes; analytics owns encoding, tests and process. A BI team that owns meaning ends up arbitrating between departments it does not run.

Is a dbt model tagged certified in its meta a certified metric? No. dbt treats meta “as metadata only, since it doesn’t affect how a resource is materialized”;[49] a tag is a claim; certification is a recorded decision against published criteria by someone other than the author. A certified table (as in Midas) is trusted data; a certified metric also fixes the definition.

A source table loaded late and a KPI was wrong for a morning. Whose incident is it? The late load is a DataOps incident; AnalyticsOps owns the consumer side: the warning on the content, the “data as of” label, the note to people who saw the number, and freshness attainment.

We changed a definition and the chart shows a step. What now? Publish a back series under the new definition, or annotate the break with its size; the UK statistics code asks for “a consistent back series where possible”.[22]

  • Interchange standards (emerging). Apache Ossie is incubating;[13] lossless exchange between vendors is unproven.
  • Warehouse-native semantics (established, evolving). Documented products;[14][15] check your BI tools’ support.
  • Assistants as metric consumers (real but limited). Governed context helps;[44] letting a model choose which metric answers a business question is speculative.
  • Metrics shared with experimentation and ML (established at scale). Airbnb serves A/B tests and reporting from the same definitions;[7] the boundary with MLOps blurs.
  • Beginner (one day): read to the glossary; Lab 1 on five dashboards; the Minerva and uMetric posts.
  • Practitioner (one to four weeks): Lab 2 in a real dbt or LookML project; cards for ten metrics; the runbook as a tabletop; then DataOps.
  • Leader or architect (a quarter): the judgement call; the decision framework; Lab 3 across the estate; then BizOps and DecisionOps.
  • Portfolio project: take one contested metric to a certified, versioned definition and retire its duplicate.
Topic What to remember
Definition An ambiguous label; here, operating the consumption layer so each important metric has one trusted definition.
Core artefacts Definition cards, a semantic layer, certification criteria, a change gate, a usage-based content review.
Decisions Who owns meaning (the business); where definitions live (decision tree); what to certify (tier 1 and 2).
Metrics Governed coverage, definition conflicts, active-content ratio, freshness attainment, time to reconcile.
Watch-outs Tool before definitions; certify everything; silent changes; retiring on views alone; short log retention.
Next actions Inventory the top 20 dashboards; name owners; export usage and delivery logs; gate tier-1 changes.

A judgement call: correct the board metric now or at quarter end?

Link to section “A judgement call: correct the board metric now or at quarter end?”

Situation (illustrative). Five weeks before quarter end, an analytics engineer finds that “Active customers”, a tier-1 board metric, counts reactivated customers twice in their return month; Lab 2’s comparison puts the overstatement at 3.1%, and last quarter’s board pack used the figure. Option A: correct now (version 3 to 4), recompute the back series, publish a correction with its size, brief the board chair. Option B: freeze until quarter end for comparability, footnote it, switch next quarter. Assumptions: a defect, not a debatable choice; not in statutory accounts; one bonus plan references it; two days of work.

The attractive choice and why it can be wrong. B looks calmer but is the wrong default for an error: every week people decide on a number known to be wrong, the bonus plan pays against it, and a footnote found later costs more trust than a correction; Facebook’s gap, once public, became a legal problem.[52][53] The UK statistics code draws the line: errors are corrected “transparently and as soon as possible”; planned method changes get advance notice and a back series.[22]

Recommendation. Choose A, with the back series and measured size; the remuneration committee decides how the bonus plan treats the restatement. Choose B only for a definition choice reasonable people would make differently (28 versus 30 days), scheduled with notice. Uncertainty: for external or regulated reporting, legal and audit may impose their own timing.

You should now be able to explain the label’s four uses; diagnose whether conflicting numbers come from definitions, data or calculation; design a card, a certification process and a change gate; choose where definitions live; and measure governance.

  1. A sales dashboard and a finance dashboard both show “Revenue”, with different values, and every pipeline test passes. What is the most likely class of problem, and whose page owns it?
Show answer for question 1

A definition problem, owned here rather than by DataOps: rival definitions, or copies of one definition that have drifted (filters, grain, time zone, refunds). Compare the definition cards first, then the queries.

  1. A pull request changes one filter in gross_revenue and nothing else. Why is that a breaking change, and what must accompany it?
Show answer for question 2

It changes gross_revenue and every metric and dashboard built on it (in Lab 2, net_revenue, refund_rate and the executive dashboard). gross_revenue needs a version bump and a change note; its dependants keep their versions but are named in the notice to affected owners, with the measured size and a back series.

  1. The quarterly review shows a 34.7% active-content ratio with a 90-day window. A manager proposes a 365-day window “to be fair”. What is the trade-off?
Show answer for question 3

It protects annual content but hides a year of unused refreshes; the ratio rises with nothing improved. Keep 90 days, explicit keep exceptions and counted deliveries, and report the window with the number.

  1. Leadership sets a target of 95% certified view share. What could go wrong?
Show answer for question 4

Certification becomes easier rather than content better: Goodhart’s law in Strathern’s form (methods). Pair the target with a sample audit of certified items and with conflict counts.

  1. An AI assistant answers “What was net revenue last month?” with a figure 2% above the dashboard. What do you check, in order?
Show answer for question 5

The definition and version used (certified or raw tables), the permissions it queried with (row-level security changes totals), the data snapshot. Then require certified metrics, definition shown, for tier-1 answers.

AnalyticsOps is a label without an owner, but the problem behind this page’s meaning is well documented: Airbnb and Uber found their teams giving different answers to simple questions, and Facebook’s advertisers learned what a gap between definition and calculation costs.

The fix is unglamorous: write down what each important number means, name who decides, encode it once, change it in the open and say by how much, show whether it is fresh and certified, and retire what nobody uses. This page’s bet: teams that treat metrics as products spend their meetings deciding; teams that treat them as by-products of dashboards keep negotiating the same numbers.

Each footnote’s “Checked” date is the day its links were last opened: 2026-10-17, or 2026-10-18 for links added or reopened in the final polish. Other dates are the source’s own. Claim-level support: research/analyticsops-ledger.md.

Changelog. 2026-10-17, r0: first draft (research, ledger, labs executed, six diagrams). 2026-10-17, r1: revised after the independent fact check: per-tool certification table; model-level change gate and measured change size; deliveries count as use; usage-log retention, attributions, CI hardening, glossary and FAQ fixes; cuts; labs re-run. 2026-10-18, post-score polish: CI profile via DBT_PROFILES_DIR, ci/profiles.yml shown; additivity; dependants’ versions; Looker 26.14; meta and benchmark wording; clearer sentences; repetition cut; after the polish check, Sources dating and the base-branch check explained.


  1. Open Data Group Rebrands as ModelOp to Lead in Enterprise ModelOps — ModelOp, 2019-10-16. Checked 2026-10-17. ↩︎

  2. What is AnalyticOps? — ModelOp, legacy Open Data Group content, undated. Checked 2026-10-17. ↩︎

  3. PyPI metadata for package “aoa” (Python client for Teradata AnalyticOps Accelerator) — Python Package Index; latest release 7.0.1, 2023-03-15. Checked 2026-10-17. ↩︎

  4. Analytic Ops: The Last Mile of Data Ops — David Menninger, Ventana Research (now ISG Software Research), 2021-11-24. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  5. Introducing AnalyticsOps — Wiiisdom, 2022-03-09 (modified 2024-07-03). Checked 2026-10-17. ↩︎ ↩︎

  6. The Journey Towards Metric Standardization — Xiaodong Wang et al., Uber Blog, 2021-01-12. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  7. How Airbnb Achieved Metric Consistency at Scale, Part I (archived copy) — Robert Chang et al., The Airbnb Tech Blog, 2021-04-30; Internet Archive snapshot of 2024-12-31 (the live page blocks automated access). Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  8. Tableau Governance Models — Tableau Blueprint, Tableau Help. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎

  9. What is LookML? — Looker documentation, Google Cloud, updated 2026-10-13. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  10. What is analytics engineering? — Claire Carroll, dbt Labs, 2019-03-26. Checked 2026-10-17. ↩︎

  11. Data Quality at Airbnb, Part 2 — A New Gold Standard (archived copy) — Vaughn Quoss, Jonathan Parks, Paul Ellwood, The Airbnb Tech Blog, 2020-11-24; Internet Archive snapshot of 2024-12-20. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  12. dbt Labs Signs Definitive Agreement to Acquire Transform, Accelerating Development of the dbt Semantic Layer — dbt Labs (PR Newswire), 2023-02-08. Checked 2026-10-17. ↩︎ ↩︎

  13. Apache Ossie (Incubating): The New Name for Open Semantic Interchange — Josh Klahr, Apache Ossie (incubating), 2026-07-10. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  14. Overview of semantic views — Snowflake Documentation. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎

  15. Unity Catalog metric views — Databricks on AWS, updated 2026-09-11. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  16. Migrate to the latest YAML spec — dbt Developer Hub. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  17. Creating metrics — dbt Developer Hub; see also Derived metrics, Ratio metrics and Measures, “Non-additive dimensions”. Checked 2026-10-18. ↩︎ ↩︎

  18. About MetricFlow — dbt Developer Hub. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  19. Add Exposures to your DAG — dbt Developer Hub. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  20. Data Processing Pipelines — The Site Reliability Workbook (Google, O’Reilly, 2018). Checked 2026-10-17. ↩︎ ↩︎

  21. Microsoft Fabric adoption roadmap: Content ownership and management — Microsoft Learn, 2024-12-30. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎

  22. Code of Practice for Statistics, Edition 3.0 (PDF) — Office for Statistics Regulation, UK Statistics Authority, Edition 3.0. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  23. Tableau Admin Guide — GitLab Handbook, last modified 2025-05-27. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  24. Introduction — Cube documentation. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  25. Microsoft Fabric adoption roadmap maturity levels — Microsoft Learn, 2024-12-30. Checked 2026-10-17. ↩︎ ↩︎

  26. ‘Improving ratings’: audit in the British University system — Marilyn Strathern, European Review 5(3):305–321, 1997; the publisher page shows the abstract, and the quoted sentence was checked in a full-text copy. Checked 2026-10-17. ↩︎ ↩︎

  27. MetricFlow repository README, “License History” — dbt Labs, GitHub; licence change commit dated 2025-10-14. Checked 2026-10-17. ↩︎

  28. MetricFlow commands — dbt Developer Hub. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎

  29. Power BI Desktop projects (PBIP) — Microsoft Learn, 2026-09-23. Checked 2026-10-17. ↩︎

  30. Validations — dbt Developer Hub. Checked 2026-10-17. ↩︎ ↩︎

  31. Using the Content Validator — Looker documentation, Google Cloud, updated 2026-10-13. Checked 2026-10-17. ↩︎ ↩︎

  32. Get started with deployment pipelines — Microsoft Learn, 2025-12-15. Checked 2026-10-17. ↩︎ ↩︎

  33. Set a Data Quality Warning — Tableau Help. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  34. Monitor usage metrics in workspaces — Microsoft Learn, 2025-12-01. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎

  35. Access the Power BI activity log — Microsoft Learn, 2024-12-30. Checked 2026-10-17. ↩︎ ↩︎

  36. Use Admin Insights to Create Custom Views — Tableau Cloud Help. Checked 2026-10-17. ↩︎ ↩︎ ↩︎

  37. Managing unused content — Looker documentation, Google Cloud, updated 2026-10-13; see also Deleted and unused content for admins. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  38. Track user activities in Microsoft Fabric — Microsoft Learn. Checked 2026-10-17. ↩︎

  39. Auditing solutions in Microsoft Purview — Microsoft Learn. Checked 2026-10-17. ↩︎

  40. Use Certification to Help Users Find Trusted Data — Tableau Cloud Help. Checked 2026-10-17. ↩︎

  41. Governance in Tableau — Tableau Blueprint, Tableau Help. Checked 2026-10-17. ↩︎ ↩︎

  42. Endorsement overview — Microsoft Learn, 2024-07-11. Checked 2026-10-17. ↩︎ ↩︎

  43. Content certification — Looker documentation, Google Cloud. Checked 2026-10-17. ↩︎

  44. A Benchmark to Understand the Role of Knowledge Graphs on Large Language Model’s Accuracy for Question Answering on Enterprise SQL Databases — Juan Sequeda, Dean Allemang, Bryon Jacob (data.world), arXiv:2311.07509, 2023-11-13. Checked 2026-10-18. ↩︎ ↩︎

  45. The Data Catalog Platform — data.world (part of ServiceNow), product home page. Checked 2026-10-17. ↩︎

  46. dbt-metricflow 0.15.0, cli_configuration.py — dbt Labs, GitHub; see also About profiles.yml — dbt Developer Hub. Checked 2026-10-18. ↩︎

  47. actions/checkout v7.0.1, action.yml (persist-credentials, default true) — GitHub. Checked 2026-10-17. ↩︎

  48. Skipping workflow runs — GitHub Docs. Checked 2026-10-17. ↩︎

  49. meta — dbt Developer Hub. Checked 2026-10-18. ↩︎ ↩︎

  50. test (LookML data tests) — Looker documentation, Google Cloud, updated 2026-10-13. Checked 2026-10-17. ↩︎

  51. Data refresh in Power BI — Microsoft Learn, 2026-09-18. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎

  52. Facebook miscalculation significantly inflated average video view times for years — Devin Coldewey, TechCrunch, 2016-09-22. Checked 2026-10-17. ↩︎ ↩︎

  53. Facebook nears $40M settlement of advertiser lawsuit — Marketing Dive, 2019-10-08. Checked 2026-10-17. ↩︎ ↩︎

Related Ops

DataOps

DataOps is the practice of delivering and running data pipelines with software discipline: changes are versioned and tested in CI, each load is checked before consumers see it, and data is monitored after, so correct data arrives on time. The label is real but not standardised.

BizOps

BizOps (business operations) has no standard definition. On this page it is the company's operating system: the planning cadence, goal and KPI hierarchy and business reviews that turn strategy into resourced work and move resources when results diverge.

DecisionOps

DecisionOps is a vendor-coined label for running automated decisions like managed software: decision logic built from rules, model scores and optimisation is versioned, tested offline and online, logged for audit and improved against measured outcomes.

DataSecOps

DataSecOps is a vendor-promoted label for building security into data platform operations: classifying data, enforcing who may see which rows and columns, masking or tokenising sensitive fields, and proving it from access logs.

ProductOps

ProductOps (product operations) gives product teams trustworthy inputs and light shared routines (product data, customer insight, planning, experiment and launch processes) so decisions rest on evidence and the product organisation scales without slowing down.

RevOps

RevOps (revenue operations) aligns marketing, sales and customer success around one set of funnel definitions, one trusted customer record, governed hand-offs and one forecast, so the revenue process can be measured and improved end to end.

ModelOps

ModelOps is the governance and lifecycle management of every model an organisation relies on (machine learning, rules, optimisation, LLM applications and agents): each one inventoried, risk-tiered, validated, approved for a use, monitored and retired.

MLOps

MLOps is the engineering practice of getting trained machine-learning models into production and keeping them correct there: versioned data, code and models, automated training and release pipelines, monitoring for skew and drift, and controlled retraining.

Practise and get certified

From xOps School

Practise AnalyticsOps and get certified

The AnalyticsOps track on xOps School (part of the Coffee Pass) has study notes, a 25-question practice quiz with explanations, and a verifiable certificate when you score 70% or more. Sign in with Google to start.

For teams

Instructor-led training

Live batches for your team, with completion certificates and reports for L&D/HR.

Source: master.md (the editorial source of this page) · built 2026-10-18 · diagrams rendered with @mermaid-js/mermaid-cli 11.17.0 (mermaid 11.17.2)