Reframing the PMO's Role in a Product Operating Model
I've spent twenty years in PMO roles, and two months leading a PMO through a shift to a Product Operating Model. I want to write about it now,...
This article describes ConstructConnect's approach to agentic analytics, drawing on the patterns Anthropic published in How Anthropic Enables Self-Service Data Analytics with Claude. Where Anthropic solved the same problems at a language model company, we solved them at a construction technology platform. The architecture is similar, the domain is different, and one decision we made differently changed the entire design.
ConstructConnect runs on a federated data system. Finance metrics live in Power BI, built by the finance team over years of careful work. Platform usage metrics were built in dbt and BigQuery. Other teams did the same: independent, parallel work across the organization. Each surface evolved on its own, each team made decisions that made sense locally, and the result is a system where the same business concept ("active account," "renewal," "an Auto Takeoff run") can mean something subtly different depending on where you look.
This is not a failure of discipline. It is what happens when capable teams build things that work. The federated system is not broken; it is the foundation. The question we needed to answer was: how do you centralize the definition layer without dismantling the work underneath it? How do you take years of Power BI development, dbt models, and institutional knowledge and make it consistent across platforms, without forcing every team onto the same tools?
The Analytics team had already made significant progress on this. Years of documentation, endorsed table guidance, and LLM-friendly context files made Claude meaningfully useful for analytical questions. But documentation is not governance. A well-written context file tells Claude what a metric is, but does not ensure that the number Claude returns matches the number Power BI shows.
The central challenge is always the same: mapping a user's question to specific, current entities in the data model. Get that mapping wrong and the answer is plausible but incorrect. Plausible-but-wrong is worse than obviously wrong. It gets used.
The first architectural decision we faced was whether to build on top of the dbt Semantic Layer (MetricFlow). We evaluated it carefully. The answer was no, and the reason shaped everything that followed.
The dbt Semantic Layer requires a live DirectQuery connection from Power BI to BigQuery. Every filter change and slicer interaction in a report fires a BigQuery query, trading Power BI's import-mode performance, and the richness of relationships and grain it can model, for metric consistency. Reports become slower and more constrained.
Beyond performance, the dbt Semantic Layer has no visibility into Power BI measures or anything outside the dbt/BigQuery stack. The moment a metric exists in more than one place (which every meaningful metric at ConstructConnect does), dbt alone cannot be the source of truth.
Any semantic layer that starts from the dbt Semantic Layer either lets Power BI drift or accepts worse report performance. This is not a decision made out of attachment to Power BI. It is a decision made because a compromised architecture was not necessary, and accepting one would have meant working around the problem rather than solving it.
Our semantic layer lives in a version-controlled repository we call semantic-one. It does two things simultaneously, and understanding both is necessary to understand why it works.
First, it is a cross-platform orchestrator. Finance metrics originated in Power BI. Platform usage metrics were built first in dbt. Each platform had its own representation of the same business concept, and those representations drifted without anyone noticing.
semantic-one holds the canonical definition for each metric and propagates changes bidirectionally. Every metric YAML includes an origin field (where the metric was first defined) and a canonical field (where it should be queried). When a metric definition changes in Power BI, it triggers an update to the semantic-one YAML, which triggers a CI job that generates updated dbt models and opens a merge request in the dbt project. Each platform receives the definition in the format it works best with: DAX for Power BI, SQL for BigQuery, plain language for Claude. No platform degrades to serve another.
Second, it is a hub built to talk to LLMs. Metric definitions in dbt YAML or Power BI DAX are written for compilers. semantic-one is written for Claude. Every metric includes a business_definition in plain language, a grain specifying the entity and time unit, a dimensions list of valid grouping fields, and a canonical_logic field with a generic SQL expression using named placeholders.
Today semantic-one holds 166 metric definitions across five product domains. The catalog is auto-generated and validated by CI on every merge.
There is a prerequisite to everything that follows that is easy to overlook: Power BI had to be treated as code before it could be governed as code.
For most of its history, Power BI was a black box. Reports and measures lived in .pbix files: binary, not diffable, not reviewable, not automatable. A measure could change and no system would know it happened. That made any kind of propagation or sync impossible. You cannot tag what you cannot read.
The move to the PBIP format changed that. PBIP stores Power BI semantic models as TMDL, plain text files that are structured, readable, and committed to version control like any other code. Every measure lives in a file. Every change produces a diff. Every merge can trigger a CI job.
That single change, moving Power BI into version control, is what unlocked the entire propagation architecture. The
// SEM_IDtag in a DAX expression is only useful because a script can now read it.
The inbound sync script works because TMDL files are just text. The governance model works because changes go through merge requests. None of this was possible before PBIP.
A design decision that matters more than it looks: three root-level fields in every metric YAML answer three distinct questions.
| Field | Question |
authority_mode |
Who owns changes? ( |
origin |
Where did this metric start? (lineage, not routing) |
canonical |
Where does Claude query this metric? (routing instruction) |
The other key schema element is endorsement. The Document Pipeline domain has both modern and legacy metrics. The legacy metrics are not duplicates; they measure a legacy pipeline that serves as the comparison baseline for the modern one. Every metric carries an endorsement field: canonical, legacy_baseline, comparison_only, or supplemental. Claude routes to canonical metrics by default and reaches for legacy baselines only when the question explicitly calls for comparison.
The metric YAMLs define what each metric means. The materialized BigQuery tables are where Claude queries them, and they solve four problems at once.
| # | Benefit | Detail |
| 01 | Fastest path to an answer | Claude queries a pre-aggregated table where the metric is already a named column, with no recomputing logic from scratch on every request. |
| 02 | Cross-platform source of truth | The same tables Claude queries are compared against Power BI measures automatically. Drift is caught before a user discovers it. |
| 03 | Testing surface for skills | Offline evaluation questions with pinned expected outputs run against governed tables, not raw sources. The tests are stable because the tables are governed. |
| 04 | Cross-platform accuracy validation | Running the same query against BigQuery and the Power BI REST API and comparing results is the most direct test of whether the semantic layer is working. |
When Claude receives an analytical question, it does not immediately write SQL. It follows a structured decision tree:
User query via Claude / Glean ↓Skill → semantic-one lookup ↓Metric found? ├── YES → Route to BigQuery materialized table (fast path) │ ↓ │ Dimension matches? │ ├── YES → Return result │ └── NO → Adjust query dimension using canonical template │ (swap time, account, user, etc.) │ (valid dimensions logged in semantic-one) │ (governed upstream tables unchanged) │ → Return result │ └── NO → Use existing query skills (no degradation) ↓ Log miss to gap table (measure name, intent, timestamp) ↓ Analytics team adds to semantic-one (next query succeeds via fast path)
Claude can flex the grouping dimension. It cannot substitute tables. The governed upstream tables never change. The canonical SQL logic never changes. Only the dimension changes, and only to one that is explicitly listed in semantic-one.
Accessing raw tables requires a deliberate, unambiguous request. A casual question routes to the governed path by default. Governed data is the default; freeform raw access is opt-in.
The semantic layer tells Claude what data exists and how to measure it. Skills tell Claude how to behave: which filters are mandatory, which tables to trust, when to stop and ask for clarification.
Our analytics plugin has 23 skills across four categories: domain query skills that encode mandatory business rules (currency normalization, internal user exclusion, mandatory filters); validation skills that verify results before they reach users; dbt development skills that translate analytical work into durable models; and discovery skills that identify where the data model is incomplete.
When a new analytics project ships, an accompanying skill or semantic-one update ships in the same cycle. The plugin has had an updated version nearly every week for the past year. As the semantic layer matures, updates to measure definitions propagate automatically through the tagging infrastructure.
Two questions tell us whether the system is working. The first: are people using it? That answer lives in our dbt models. The second: are we meeting demand? That answer lives in the gap log.
Every time Claude receives a question and cannot find the measure in semantic-one, it logs the request to a BigQuery table. The analytics team models that data. Gaps surface in reporting. Frequently-requested but undefined metrics get prioritized and added to semantic-one. The next time a user asks the same question, it routes through the fast path.
The initial focus has been Power BI, because that is where the most work has already been done. But the tagging system is origin-agnostic by design. A metric can be defined first in dbt, first in semantic-one directly, or first in Power BI. The tag travels with the definition regardless of where it started. You can define a metric in semantic-one and propagate it into Power BI. You can define it in dbt and surface it there. The system does not privilege any platform as the starting point.
Every Power BI measure is tagged with a // SEM_ID=<metric_id> comment in the DAX expression. A sync script reads the Power BI TMDL files, finds the SEM_ID tag, and maps each measure to its YAML. A change in one platform propagates to all others. The SEM_ID is the spine that connects three representations of the same metric (DAX, SQL, and plain language) into one governed definition.
Gap logging extends beyond question-answering. Periodic scans of the Power BI and dbt repositories surface measures that are not yet tagged, definitions that overlap with existing semantic-one entries, and metrics that exist in one platform but not another. The gap log is a continuous audit of the federated system itself.
When a metric definition changes in semantic-one and merges to main, the CI pipeline generates the updated dbt models and opens a merge request in the dbt project. That is the limit of what a pipeline should own. That is where the agent takes over.
After the CI pipeline opens the merge request, an agentic workflow picks up the branch. Its job is not to generate; the pipeline already did that. Its job is to validate, polish, and certify before a human ever sees the MR.
The pipeline creates the work; the agent certifies it; the human approves it.
|
❌ Before Answering "how many active Takeoff Boost accounts renewed last quarter?" required a data analyst who knew which table to use, which filters to apply, and whether the Power BI number and the SQL number were actually computing the same thing. |
|
⚠️ Plugin, no semantic layer Claude can answer by querying endorsed tables with mandatory filters. The answer is usually correct, but depends on the skill being current and the user asking a question the skill author anticipated. |
|
✅ With semantic layer Claude looks up the metric in semantic-one, finds |
Anthropic reports 95% accuracy on business analytics queries with their semantic layer in place. We are building toward the same target. The gap log tells us exactly how much of our data estate still falls in the gap.
Self-service analytics fails, usually, not because the tools are wrong but because the layer between question and data has too many moving parts and no one owns all of them. The semantic layer is that layer, governed and versioned. When it works, the analyst stops being the person who knows which table to use and becomes the person who decides what to measure next. That is the shift we are building toward.
Built by the ConstructConnect Analytics Engineering Team. The semantic layer architecture described here was informed by Anthropic's published approach to self-service analytics with Claude, adapted for a multi-platform environment where no single data tool can serve as the authoritative source across Power BI, dbt, BigQuery, and AI agents simultaneously.
I've spent twenty years in PMO roles, and two months leading a PMO through a shift to a Product Operating Model. I want to write about it now,...