What AnalyticsOps is
Link to section “What AnalyticsOps is”In 30 seconds
Link to section “In 30 seconds”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.
At a glance
Link to section “At a glance”| 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. |
The problem AnalyticsOps solves
Link to section “The problem AnalyticsOps solves”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”Origins and evolution
Link to section “Origins and evolution”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] |
Glossary and mental model
Link to section “Glossary and mental model”| 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).
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.
How AnalyticsOps relates to other Ops
Link to section “How AnalyticsOps relates to other Ops”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 |
Principles and operating model
Link to section “Principles and operating model”A proposed charter and principles
Link to section “A proposed charter and principles”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.
- Define once, serve everywhere. Airbnb’s stated vision for Minerva was “define metrics once, use them everywhere”.[7]
- 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]
- The business owns meaning; analytics owns encoding. Finance decides what “net revenue” includes; an analytics engineer encodes and tests it.
- Discipline at the core, flexibility at the edge. Microsoft’s phrase for managed self-service.[21]
- 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]
- Trust is visible on the content. Badge, owner, version and freshness appear where people read the number.
- 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).
Building blocks
Link to section “Building blocks”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.
Roles, ownership and RACI
Link to section “Roles, ownership and RACI”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 |
Lifecycle of a metric
Link to section “Lifecycle of a metric”Each state means something to consumers; it lives in the definition’s metadata (a status key in Lab 2).
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]
Operating model and governance
Link to section “Operating model and governance”- 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]
Architecture and workflow
Link to section “Architecture and workflow”Reference architecture
Link to section “Reference architecture”A data plane (tables, semantic layer, consumers) and a control plane (Git, CI, telemetry, 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”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]
Practices, frameworks and tools
Link to section “Practices, frameworks and tools”Core practices and playbooks
Link to section “Core practices and playbooks”| 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 |
Playbook: certifying a metric
Link to section “Playbook: certifying a metric”- Card. The owner writes the definition in business language (template); Midas design specs likewise start with “plain-text business definitions”.[11]
- 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.
- 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]
- Publish. Badge, owner, version and consumers visible. Rollback: revert the pull request; the previous version stays.
Methods, standards and frameworks
Link to section “Methods, standards and frameworks”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]
Tooling landscape
Link to section “Tooling landscape”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.
What each tool can certify
Link to section “What each tool can certify”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 and AI
Link to section “Automation and AI”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.
AnalyticsOps in practice
Link to section “AnalyticsOps in practice”Reference implementation
Link to section “Reference implementation”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]
Hands-on labs
Link to section “Hands-on labs”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.
- Save the inventory below as
metric-inventory.csv(illustrative data; filters joined with&) and the script asmetric_conflicts.py. - Run
python3 metric_conflicts.py metric-inventory.csv; echo $?. - 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.
- Save the YAML below as
base.ymland the gate asmetric_change_gate.py. - Copy
base.ymltohead.yml; inhead.ymlchange thegross_revenuefilter to{{ Dimension('order__status') }} in ('completed', 'partially_refunded')and therefundslabel toRefunds (EUR). - Run
python3 metric_change_gate.py base.yml head.yml; echo $?(expected: blocked). - Copy
head.ymltohead-fixed.yml; there, setgross_revenue’sdefinition_versionto 4 and addchange_note: "2026-11-01: include partially refunded orders (FIN-2041)"; run the gate on it (expected: ok). - Save
base.ymlwithout the top-levelrefund_ratemetric ashead-removed.yml; run the gate (expected: blocked). - Save
base.ymlashead-time.ymlwith the model’sagg_time_dimensionset toshipped_atand ashipped_attime column added; run the gate (expected: blocked). Adding a dimension alone passes. - Optional, in a real dbt project:
dbt parse, thenmf validate-configs --skip-dw(self-hosted) ordbt 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.
- 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]
- Run the review with the export date.
- Notify owners (14 days to object); archive by soft delete; reassign orphans; send certified-but-unused items to the certifier.
- 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.
Use cases and scenarios
Link to section “Use cases and scenarios”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.
Measure, govern, pay
Link to section “Measure, govern, pay”Metrics and KPIs
Link to section “Metrics and KPIs”| 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.
Worked calculation (illustrative)
Link to section “Worked calculation (illustrative)”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.
Quality, security, privacy and compliance
Link to section “Quality, security, privacy and compliance”| 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, capacity and sustainability
Link to section “Cost, capacity and sustainability”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.
Assurance of the operating model
Link to section “Assurance of the operating model”- 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.
When things go wrong
Link to section “When things go wrong”Common failures and troubleshooting
Link to section “Common failures and troubleshooting”| 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.
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.
- Label (first 30 minutes). A banner on both dashboards so nobody forwards either number; say when an answer is due.
- 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.
- 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.
- 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.
- Communicate (same day): both numbers and definitions, the canonical one, what changes where and when.
- Prevent (two weeks): catalogue both metrics; add Lab 1 to the monthly review; certify the canonical metric; gate the product team’s models.
Anti-patterns and myths
Link to section “Anti-patterns and myths”| 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) |
Deciding and adopting
Link to section “Deciding and adopting”When to use and when not to use
Link to section “When to use and when not to use”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.
Decision framework
Link to section “Decision framework”First decide whether to adopt a semantic layer at all, then where definitions should live; for dbt users the plan decides (tooling landscape).
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.
Weighted comparison of the options
Link to section “Weighted comparison of the options”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.
Maturity model
Link to section “Maturity model”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.
Adoption roadmap: 30/60/90 days
Link to section “Adoption roadmap: 30/60/90 days”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.
Toolkit
Link to section “Toolkit”Checklists
Link to section “Checklists”Preflight for a new metric
Link to section “Preflight for a new metric”- 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.
Certification
Link to section “Certification”- 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.
Quarterly content review
Link to section “Quarterly content review”- 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.
Closing a conflicting-number incident
Link to section “Closing a conflicting-number incident”- Class recorded (definition, data, calculation); explanation sent to all who saw both numbers.
- Canonical metric certified; the other renamed or retired; prevention step merged.
Templates
Link to section “Templates”Metric definition card
Link to section “Metric definition card”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>
Breaking-change notice
Link to section “Breaking-change notice”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 retirement notice
Link to section “Content retirement notice”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>.
Certification criteria (scorecard)
Link to section “Certification criteria (scorecard)”| 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]
Going further
Link to section “Going further”Advanced topics and emerging direction
Link to section “Advanced topics and emerging direction”- 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.
Learning path
Link to section “Learning path”- 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.
Wrap-up
Link to section “Wrap-up”Executive recap and cheat sheet
Link to section “Executive recap and cheat sheet”| 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.
Learning objectives and mastery check
Link to section “Learning objectives and mastery check”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.
- 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.
- A pull request changes one filter in
gross_revenueand 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.
- 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.
- 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.
- 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.
Conclusion
Link to section “Conclusion”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.
Sources and further reading
Link to section “Sources and further reading”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.
Open Data Group Rebrands as ModelOp to Lead in Enterprise ModelOps — ModelOp, 2019-10-16. Checked 2026-10-17. ↩︎
What is AnalyticOps? — ModelOp, legacy Open Data Group content, undated. Checked 2026-10-17. ↩︎
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. ↩︎
Analytic Ops: The Last Mile of Data Ops — David Menninger, Ventana Research (now ISG Software Research), 2021-11-24. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Introducing AnalyticsOps — Wiiisdom, 2022-03-09 (modified 2024-07-03). Checked 2026-10-17. ↩︎ ↩︎
The Journey Towards Metric Standardization — Xiaodong Wang et al., Uber Blog, 2021-01-12. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
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. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Tableau Governance Models — Tableau Blueprint, Tableau Help. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎
What is LookML? — Looker documentation, Google Cloud, updated 2026-10-13. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
What is analytics engineering? — Claire Carroll, dbt Labs, 2019-03-26. Checked 2026-10-17. ↩︎
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. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
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. ↩︎ ↩︎
Apache Ossie (Incubating): The New Name for Open Semantic Interchange — Josh Klahr, Apache Ossie (incubating), 2026-07-10. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Overview of semantic views — Snowflake Documentation. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎
Unity Catalog metric views — Databricks on AWS, updated 2026-09-11. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Migrate to the latest YAML spec — dbt Developer Hub. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Creating metrics — dbt Developer Hub; see also Derived metrics, Ratio metrics and Measures, “Non-additive dimensions”. Checked 2026-10-18. ↩︎ ↩︎
About MetricFlow — dbt Developer Hub. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Add Exposures to your DAG — dbt Developer Hub. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Data Processing Pipelines — The Site Reliability Workbook (Google, O’Reilly, 2018). Checked 2026-10-17. ↩︎ ↩︎
Microsoft Fabric adoption roadmap: Content ownership and management — Microsoft Learn, 2024-12-30. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎
Code of Practice for Statistics, Edition 3.0 (PDF) — Office for Statistics Regulation, UK Statistics Authority, Edition 3.0. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Tableau Admin Guide — GitLab Handbook, last modified 2025-05-27. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Introduction — Cube documentation. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Microsoft Fabric adoption roadmap maturity levels — Microsoft Learn, 2024-12-30. Checked 2026-10-17. ↩︎ ↩︎
‘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. ↩︎ ↩︎
MetricFlow repository README, “License History” — dbt Labs, GitHub; licence change commit dated 2025-10-14. Checked 2026-10-17. ↩︎
MetricFlow commands — dbt Developer Hub. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎
Power BI Desktop projects (PBIP) — Microsoft Learn, 2026-09-23. Checked 2026-10-17. ↩︎
Validations — dbt Developer Hub. Checked 2026-10-17. ↩︎ ↩︎
Using the Content Validator — Looker documentation, Google Cloud, updated 2026-10-13. Checked 2026-10-17. ↩︎ ↩︎
Get started with deployment pipelines — Microsoft Learn, 2025-12-15. Checked 2026-10-17. ↩︎ ↩︎
Set a Data Quality Warning — Tableau Help. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Monitor usage metrics in workspaces — Microsoft Learn, 2025-12-01. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎
Access the Power BI activity log — Microsoft Learn, 2024-12-30. Checked 2026-10-17. ↩︎ ↩︎
Use Admin Insights to Create Custom Views — Tableau Cloud Help. Checked 2026-10-17. ↩︎ ↩︎ ↩︎
Managing unused content — Looker documentation, Google Cloud, updated 2026-10-13; see also Deleted and unused content for admins. Checked 2026-10-18. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Track user activities in Microsoft Fabric — Microsoft Learn. Checked 2026-10-17. ↩︎
Auditing solutions in Microsoft Purview — Microsoft Learn. Checked 2026-10-17. ↩︎
Use Certification to Help Users Find Trusted Data — Tableau Cloud Help. Checked 2026-10-17. ↩︎
Governance in Tableau — Tableau Blueprint, Tableau Help. Checked 2026-10-17. ↩︎ ↩︎
Endorsement overview — Microsoft Learn, 2024-07-11. Checked 2026-10-17. ↩︎ ↩︎
Content certification — Looker documentation, Google Cloud. Checked 2026-10-17. ↩︎
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. ↩︎ ↩︎
The Data Catalog Platform — data.world (part of ServiceNow), product home page. Checked 2026-10-17. ↩︎
dbt-metricflow 0.15.0,
cli_configuration.py— dbt Labs, GitHub; see also About profiles.yml — dbt Developer Hub. Checked 2026-10-18. ↩︎actions/checkout v7.0.1, action.yml (
persist-credentials, defaulttrue) — GitHub. Checked 2026-10-17. ↩︎Skipping workflow runs — GitHub Docs. Checked 2026-10-17. ↩︎
test (LookML data tests) — Looker documentation, Google Cloud, updated 2026-10-13. Checked 2026-10-17. ↩︎
Data refresh in Power BI — Microsoft Learn, 2026-09-18. Checked 2026-10-17. ↩︎ ↩︎ ↩︎ ↩︎
Facebook miscalculation significantly inflated average video view times for years — Devin Coldewey, TechCrunch, 2016-09-22. Checked 2026-10-17. ↩︎ ↩︎
Facebook nears $40M settlement of advertiser lawsuit — Marketing Dive, 2019-10-08. Checked 2026-10-17. ↩︎ ↩︎