One retail dataset, two ways to model it, three data agents, and an answer key computed before a single question was asked.
Contents
- Executive Summary
- 1. Data Agents in Fabric
- 2. Two Semantic Layers
- 3. What the Positioning Predicts
- 4. Test Design
- 5. Traversal Questions
- 6. Aggregation Questions
- 7. MasterAgent
- 8. The Boundary Case
- 9. Discussion
- 10. What These Results Point To
- 11. Conclusion
Executive Summary
An AI agent that answers confidently but wrongly creates governance risk. We tested which Fabric semantic layer keeps data agents accurate, and what it takes to make one agent reliable across every kind of business question.
- We took one retail dataset and modeled it in two shapes: a Fabric IQ ontology and a conventional star schema semantic model. Each shape got its own Fabric data agent, named OntologyBasedAgent and StarBasedAgent, and a third agent, MasterAgent, received both sources at once.
- Every answer was graded against ground truth computed independently from the raw data before any agent saw a question, and every question was tested repeatedly for stability.
- On relationship traversal questions, OntologyBasedAgent answered everything correctly while StarBasedAgent failed every question at least part of the time, repeatedly fabricating carrier and warehouse names that appear nowhere in the correct answers.
- On aggregation and ranking questions the mirror image appeared: StarBasedAgent was exact and OntologyBasedAgent failed every question, usually returning a confident wrong number.
- The generated DAX and GQL explain both patterns: each translation layer is dependable inside its native style of query and improvises whenever the question, or the model, offers no explicit path.
- MasterAgent, holding both sources behind carefully written routing instructions, answered every question class that either single source failed, at a measurable price in response time and with one residual risk that no configuration removes.
| Question family | StarBasedAgent | OntologyBasedAgent | MasterAgent |
|---|---|---|---|
| Relationship traversal (follow one order, customer or return through its connections) | Failed | Passed | Passed |
| Aggregation and ranking (rates, counts, thresholds, time buckets, sequences) | Passed | Failed | Passed |
| Mixed questions needing both families in one answer (section 7.2) | Failed | Failed | Passed |
| The boundary case (section 8) | Inconsistent | Inconsistent | Inconsistent |
1. Background: Data Agents in Microsoft Fabric
A Fabric data agent is a conversational layer you configure over Fabric data items. At run time an orchestrator parses the user’s question, selects a data source, invokes that source’s query generation tool, validates and executes the query, and composes a natural-language answer from the result. [1]

One agent can hold up to five data sources in any combination, and each source category is served by a different query generation service speaking a different language: [2] [3]
| Source category | Query language | Positioning in Microsoft’s documentation |
|---|---|---|
| Lakehouse, Warehouse, SQL DB, mirrored DB | T-SQL | Structured analytics over relational and Delta Lake data |
| Eventhouse KQL database | KQL | Real-time intelligence and time-series analytics |
| Power BI semantic model | DAX | Business logic, calculated measures, curated metrics |
| Graph model (preview) | GQL | Relationship-rich data exploration and graph analytics |
| Ontology (preview) | Ontology-native, resolved to GQL or KQL | Domain knowledge and semantic context |
Five source categories, five different translators. This article is about the two of them that compete for the same job. The Power BI semantic model and the ontology are both semantic layers: both describe the business to the agent, both carry definitions and connections on top of the raw tables, and both promise to turn a business user’s plain question into a correct query. But they are answered by completely different translators: a natural-language-to-DAX service that thinks in tables, filters and measures, and a natural-language-to-ontology layer that thinks in entities and named relationships. [3] [4]
That difference defines the experiment you are about to read. We took one retail dataset and modeled it both ways, attached each model to its own data agent, wrote one set of business questions, and computed every correct answer before any agent saw a question. The rest of this article walks through what each agent got right, what each got wrong, why the generated queries say it had to be that way, and what happened when we put both models behind a single agent.
2. Power BI Semantic Model vs. Fabric IQ Ontology: Two Semantic Layers
2.1 The Established Layer: The Power BI Semantic Model
The semantic model is the mature semantic layer of the Microsoft data stack, refined over more than a decade of Power BI practice, and Microsoft positions it as the data agent source for business logic, calculated measures and curated metrics. It describes the business as fact tables carrying the numbers and dimension tables giving them context, joined by relationships whose purpose is to propagate filters: a product dimension filters a sales fact, a date dimension filters everything. Measures encode the business logic once so every consumer computes it the same way, and when a data agent sits on top, the built-in natural-language-to-DAX service translates questions using the model’s metadata, its relationships and its measures. [3] [9]
It is a superb design for what it was built for, and one property of that design shapes everything that follows: its relationships are filters, not sentences. A star schema can say products slice sales. It has no native way to say a return was raised against this order, or this shipment fulfilled that order, because in a strictly modeled star, fact tables do not relate to fact tables at all. Those connections ride along as shared key columns that no relationship formalizes; the context stays, in Microsoft’s own phrase about the alternative, “buried in join logic.” [4] [11]
2.2 The New Layer: Ontology in Fabric IQ
The ontology item, part of the Fabric IQ workload and currently in preview, is Microsoft’s answer to that gap. Where a semantic model describes tables, an ontology describes the business itself: the things in it become entity types, the facts about those things become properties, and the ways they connect become relationships, each one named, directional and governed so that every team means the same thing by the same word. Data binding then points those definitions at real tables in OneLake without copying a row, and Fabric builds a queryable graph out of the bindings, turning the connections themselves into something an agent can walk. [4]
The design intent is the part worth underlining, and the documentation says it plainly: relationships exist to make context explicit rather than, in Microsoft’s phrase, “buried in join logic.” A star schema asks every query to reconstruct how the business fits together. An ontology writes it down once. [4]
Our ontology defines sixteen entity types and the full web of relationships between them. A few examples show how they read: an Order is placedBy a Customer; a Shipment fulfills an Order and is carriedBy a Carrier; a Return is returnedFor an Order and returns a Product; an OrderLine belongsTo an Order and references a Product. Each one is a named, directional, traversable connection in the model itself, exactly the sentences the star schema has no way to store.
On top of this sits the query experience. Questions are asked in the ontology’s own terminology, and a natural-language-to-ontology layer converts them into structured queries, with the documentation promising that filters, joins and definitions will align with what the ontology publishes rather than with how the data happens to be stored. [4]
The architectural bet is easy to state. In a star schema, the knowledge of how business objects connect lives with whoever writes the query. In an ontology, it lives in the model. If that bet pays off, an agent walking from a return to its carrier should never have to guess a join, because the path is already drawn.
3. What Microsoft's Positioning Predicts for Data Agents
Microsoft’s data source documentation assigns each grounding a role: semantic models are positioned for business logic, calculated measures and curated metrics, while graph-backed sources are positioned for relationship-rich data exploration. The ontology documentation goes further, presenting the ontology as the semantic backbone that lets agents reason over business definitions rather than raw storage. [3] [4]
Taken together, the positioning implies something concrete and measurable: an agent grounded in an ontology should outperform an agent grounded in a star schema whenever a question hops across connected business objects, because the star deliberately keeps its fact tables apart while the graph keeps everything joined by design.
We formalized the claim into two testable hypotheses. Hypothesis one: on questions that traverse relationships from a single record, OntologyBasedAgent should outperform StarBasedAgent. Hypothesis two: on questions that aggregate, rank or filter across the whole dataset, the two agents should perform comparably, since both models hold identical data and neither positioning statement predicts a gap there. The results upheld the first hypothesis and refuted the second, in a direction the positioning does not anticipate. That refutation turned out to be the most instructive result of the study.
4. Test Design: One Dataset, a Star Schema and an Ontology
4.1 One Dataset, Two Models
The domain is retail cold-chain logistics: customers place orders, warehouses ship them through carriers, shipments carry transit times and delay reasons, items come back as returns, and inventory, forecasts and demand signals describe the supply side. The data covers 100,000 orders, 10,006 returns and 84,972 shipments, and every order has exactly one shipment, a property we verified and relied on while locking answers.
From that single source we derived two models, following each paradigm’s own rulebook rather than bending either toward the other.

For the star schema, the flat operational data was decomposed into seven fact tables and eight dimensions. Facts relate to dimensions and to nothing else: no fact joins a fact, no dimension joins a dimension, and transaction identifiers such as the order number remain degenerate dimensions, exactly as dimensional modeling guidance prescribes. Where the ontology reaches reference data through a relationship, the star flattens it: the category’s handling limits, for example, live as columns on the product dimension. [11]

For the ontology, the same data became sixteen entity types and a web of named relationships. Nothing is flattened; every connection that the business would describe in a sentence exists as an explicit, traversable edge. This is the complete map:

Behind every node on that map is a fully configured entity. Here is the Return entity in Fabric IQ: its properties, the returnId key, and the relationships panel where returnedFor and returns are declared as first-class, directional edges, the same connections the map draws between Return, Order and Product.

Preparation effort was kept deliberately equal on both sides. The semantic model was configured through Power BI’s Prep data for AI capability: curated descriptions, synonyms and verified, human-readable naming across its tables and columns. The ontology’s entities, properties and relationships carry descriptions and synonyms of the same depth. The comparison therefore starts from two models that are equally well documented, and nothing in the rest of this article can be traced to one side simply being described better than the other. [9]
4.2 Three Agents
Three data agents were created and are named throughout this article exactly as they were named in Fabric. OntologyBasedAgent holds the ontology as its only source. StarBasedAgent holds the semantic model as its only source. MasterAgent, introduced in section 7, holds both. OntologyBasedAgent and StarBasedAgent received instructions of equal depth: the same business definitions, the same formatting rules, and the documented workaround instruction for a known ontology aggregation issue. Neither received query patterns, join hints, or any text that would pre-solve a specific question. [5]
4.3 Protocol
The protocol had four rules. First, ground truth before questions: every answer was computed with an independent script directly against the source files, and cross-checked to be identical in both models’ data, before any agent saw the question. Second, questions were phrased the way a business user asks them, in plain language with no internal identifiers, and each scored question was constructed so that exactly one correct answer exists, which is what makes strict grading possible; how the agents behave when a question is genuinely ambiguous, for example a customer name shared by several people, was tested deliberately as its own scenario rather than left to distort the scoring. Third, every question was tested multiple times for stability, so no verdict here reflects a single lucky or unlucky response; where behavior genuinely varied between attempts, the article says so and calls it inconsistent. Fourth, grading was binary against the locked answer: an answer that is partially right, or right with fabricated additions, is a fail.
The full program was considerably wider than the questions detailed below. Question classes where both agents answered correctly every time, including date arithmetic, negation, set intersection, shared-entity checks and simple lookups, are omitted from the detailed sections because they do not separate the agents. What follows are the classes that do.
4.4 How the Test Design Addresses Validity
Any benchmark of natural-language systems carries four inherent risks, and the design above exists to answer them. Ambiguity: language permits multiple readings, which is why the scored set only contains questions with a single defensible answer, and why ambiguity handling was examined as its own scenario. Leakage: instructions influence agents, which is why both agents’ instructions hold business definitions and formatting rules only, reviewed against every question in the set. Non-determinism: generative translation can produce a different query for the same words, which is why no verdict rests on one response. Currency: the ontology stack is in preview and moving, which is why every product behavior claimed here is tied to a citation the reader can check against the current documentation.
5. Where the Ontology Earns Its Keep: Traversal Questions
The first family of questions plays directly to the ontology’s declared strength. The scenario is a customer support escalation: someone needs the story behind one person’s returns, what came back, why, which carrier delivered it, from which warehouse, and whether the shipment ran late. Nobody opens a dashboard for this. They ask. Five questions of exactly this shape went to both agents, phrased the way a support lead would phrase them.
| Question (verbatim) | Ground truth | StarBasedAgent | OntologyBasedAgent |
|---|---|---|---|
| What did Mark Hill return, why, and which carrier shipped each of those orders? | Pain Relief Gel, 90 ct, late delivery, ThermoShip. Stir-Fry Veg Mix, 750g, quality issue, NovaCold. | Fail Items and reasons correct, then stated the data does not link a carrier to these returns. | Pass Complete and exact. |
| What did Karen Thompson return, why, and which carrier shipped each of those orders? | Frozen Blueberries, 250g, damaged, NovaCold. Stir-Fry Veg Mix, 1kg, quality issue, NovaCold. | Inconsistent Exact in some attempts; in others both orders attributed to ValueShip, a carrier that shipped neither. | Pass Complete and exact. |
| One of Michelle Torres’s returned items was on a delayed shipment. Which item, which carrier, and why the delay? | Whole Wheat Bread, Value Pack; ThermoShip; carrier capacity shortage. | Fail Reported items that were not delayed, all attributed to ValueShip. | Pass |
| One of Mary Miller’s returned items was on a delayed shipment. Which item, which carrier, and why the delay? | Gel Pens 12pk; ThermoShip; route congestion. | Inconsistent Sometimes exact, sometimes the right item attributed again to ValueShip. | Pass |
| What did Nancy Lopez return, why, and from which warehouse was each of those orders shipped? | Catnip Mice, Family Pack, late delivery; Binder Set, 2-Pack, no longer needed; both from Southwest DC 1. | Fail Items and reasons correct, warehouse reported as West Coast DC 8. | Pass |
5.1 Reading the Queries: An Improvised Bridge against a Stored Path
OntologyBasedAgent’s clean sweep needs little forensic work: every hop these questions require exists in its model as a named edge, and the generated GQL simply walked them. The interesting question is why StarBasedAgent, sitting on identical data, kept getting hurt. The generated queries tell that story in three acts.

| Question | What the generated DAX actually did |
|---|---|
| Mark Hill: returns and carriers | Improvised bridge between the return and shipment facts came back empty; the carrier was reported as not linked in the data. |
| Karen Thompson: returns and carriers | Exact in some attempts; in the failing attempt the same bridge misfired and MAX over the unfiltered carrier table returned ValueShip for both orders. |
| Michelle Torres: delayed item and carrier | The shipment filter never reached the carrier dimension; MAX over the unfiltered table returned the alphabetically last carrier. |
| Mary Miller: delayed item and carrier | Exact in some attempts; failing attempts repeated the same alphabetically last carrier. |
| Nancy Lopez: returns and warehouses | The same mechanism aimed at the warehouse dimension; the alphabetically last warehouse name came back. |
Three code samples cover all five rows, because the five failures reduce to two mechanisms plus one proof that the engine could have succeeded.
Act one: the hop that does not exist. In a star schema built by the book, fact tables never join each other, so the model contains no path from returns to shipments. Every question above needs that path. The DAX translator therefore has to invent a bridge at query time, and here is the bridge it built for the Mark Hill question:
DAX · generated by StarBasedAgent
VAR _MarkHillReturns =
CALCULATETABLE ( 'factreturn',
'dimcustomer'[customerName] = "Mark Hill" )
-- inside ADDCOLUMNS, per returned row:
"Carrier Name",
VAR _OrderShipments =
CALCULATETABLE ( 'factshipment',
TREATAS ( VALUES ( 'factreturn'[orderId] ),
'factshipment'[orderId] ) )
RETURN
MAXX ( _OrderShipments, RELATED ( 'dimcarrier'[carrierName] ) )
The intent is right: carry each return’s order number into the shipment fact. Evaluated inside a row context, the bridge silently produced nothing, the carrier column rendered blank, and the agent wrote a fluent conclusion on top of the empty column: the data does not link a carrier to these returns. The data is complete. The query was not. To anyone reading the chat, this failure is indistinguishable from a data quality problem, which is what makes it operationally dangerous.

Act two: the phantom carrier. For the carrier and warehouse questions the translator chose a different shape, and this one does not fail silently. It fails creatively:
DAX · generated by StarBasedAgent
"Carrier Name",
CALCULATE (
MAX ( 'dimcarrier'[carrierName] ),
'factshipment'[isDelayed] = TRUE,
TREATAS ( VALUES ( 'factreturn'[orderId] ),
'factshipment'[orderId] ) )
When the shipment filter fails to reach the carrier dimension, that MAX runs over the entire unfiltered carrier table, and the maximum of a text column is simply the alphabetically last value. ValueShip is the last carrier name in the alphabet. West Coast DC 8 sorts after every other warehouse. Across four different questions and two different dimensions, the wrong answers followed a pattern: the bottom row of a list, delivered with complete confidence.
The wrong answers followed a pattern: the alphabetically last name in the table, presented as fact.

Act three: proof that the engine is capable. In attempts where the translator happened to choose explicit predicate filters instead of a row-context bridge, the very same question came back exact:
DAX · generated by StarBasedAgent in a passing attempt
"Line Value",
CALCULATE (
SUM ( 'factsales'[lineTotal] ),
FILTER ( ALL ( 'factsales' ),
'factsales'[orderId] = _OrderId
&& 'factsales'[productId] = _ProductId ) )
DAX can express this join. The model just never tells the translator how, so correctness depends on which idiom it happens to reach for on a given attempt. That is the precise sense in which the weakness is structural rather than a bug: the dice are built into the missing relationship.
The other half of the story is why OntologyBasedAgent never had to roll those dice. Here is the query it generated for the Karen Thompson question, and the contrast with everything above is the entire point:
GQL · generated by OntologyBasedAgent
MATCH (o:Order)-[:placedBy]->(c:Customer),
(s:Shipment)-[:fulfills]->(o),
(s)-[:carriedBy]->(car:Carrier),
(r:Return)-[:returnedFor]->(o),
(r)-[:returns]->(p:Product)
WHERE LOWER(c.name) = LOWER("Karen Thompson")
RETURN p.name, r.reason, car.carrierName
There is nothing to dissect, and that is the finding. Every hop in this query is a named relationship read straight from the model: placedBy, fulfills, carriedBy, returnedFor, returns. The translator transcribes the model rather than constructing a join. Nothing is improvised, so there is nothing to improvise wrongly, and the same query shape came back correct on every anchor in every attempt. Where the star’s translator must invent the route each time, the ontology’s translator copies a route that was drawn once.
5.2 Verdict
Hypothesis one holds, and not narrowly. OntologyBasedAgent answered every traversal question correctly every time. StarBasedAgent failed each of them at least part of the time, and its characteristic failures either blame the data or fabricate an entity. If your users ask “what happened” questions, this gap decides the outcome. But “what happened” questions are only half a workload, and the other half turned the tables completely.
6. Where the Star Strikes Back: Aggregation and Ranking Questions
Now move from the support desk to the Monday operations review. Which carrier has the worst return rate. Which quarter was worst. How many customers keep coming back with returns, and which of them matter most. These questions sweep the entire dataset, group it, filter on the group results and rank. Six of them went to both agents.
| Question (verbatim) | Ground truth | OntologyBasedAgent | StarBasedAgent |
|---|---|---|---|
| For each carrier, what percentage of the orders they shipped had an item returned, and which carrier has the highest rate? | NovaCold, 26.9 percent, 2,739 of 10,170 shipped orders. | Fail Reported 100 percent for every carrier and suggested the dataset only records returned orders. | Pass Exact table for all fifteen carriers and the correct winner. |
| In which calendar quarter were the most items returned, and how many? | Q4 2023, 1,348 items. | Fail Answers built from a handful of order lines, one attempt naming a quarter with five items. | Pass |
| How many customers have made more than one return? | 2,652. | Fail Answered 6,229, which is every customer with at least one return. | Pass |
| Among customers who have made more than one return, who has the highest lifetime value? | Emily Walker, $99,921.21. | Inconsistent One attempt answered a different question entirely, the customer with the most returns. | Inconsistent Usually exact; one attempt appended invented names claiming a tie. |
| Has James Johnson placed any orders since his most recent return? If so, how many? | Yes, two. | Fail Reported that no orders were placed since. | Pass |
| How many carriers have a return rate above 20 percent, and which are they? | Two: NovaCold and ThermoShip. | Fail Returned all fifteen carriers at 100 percent. | Pass |
6.1 Reading the Queries: Paths Where Sets Are Needed
Six questions, six wrong answers from the agent that had just swept the traversal round. Before assuming the worst, we did what section 5 did in reverse: read every generated query. The six failures trace back to four mechanisms, each traceable to specific questions and each checkable against Microsoft’s own documentation.
| Mechanism | Questions it broke | In one sentence |
|---|---|---|
| The vanishing denominator | Return rate per carrier; carriers above 20 percent; orders since the last return | The query pattern only sees orders that have returns, so everything else stops existing. |
| The filter that never arrives | Customers with more than one return; highest lifetime value among them | The threshold is dropped, or turned into a sort order, so a different question gets answered. |
| Grouping by the unique key | Quarter with the most returned items | Every group holds exactly one row, so every count collapses to one. |
| The fabricated constant | A ranking question from the wider program | The query projects a hardcoded zero instead of fetching the value, then ranks by it. |
Mechanism one, the vanishing denominator, decided the carrier rate question, the 20 percent threshold question and the James Johnson question. A rate needs two populations, everything shipped and everything returned. But a MATCH pattern is an inner join: whatever is not on the pattern does not exist. Watch the denominator disappear in the rate query:
GQL · generated by OntologyBasedAgent for the rate question
MATCH (s:Shipment)-[:fulfills]->(o:Order),
(s)-[:carriedBy]->(c:Carrier),
(r:Return)-[:returnedFor]->(o)
RETURN c.carrierName,
COUNT(DISTINCT o.orderId) AS orders_with_return_count

Every order in this query’s world has a return, because the pattern demands one. Numerator equals denominator, every carrier scores 100 percent, and the agent, noticing the absurdity, blamed the data instead of the query. The sharpest part: GQL has the fix built in. OPTIONAL MATCH, the left join designed for exactly this, is fully supported in Fabric’s implementation. The translation layer simply never wrote it, in any of our attempts. The language is capable; the translator does not reach for the right tool. [6] [7]

Mechanism two is the filter that never arrives. Asked for customers with more than one return, the generated query counts customers with any return, and, in a detail that says everything, names its output column as if the filter existed:
GQL · generated by OntologyBasedAgent
MATCH (r:Return)-[:returnedFor]->(o:Order)-[:placedBy]->(c:Customer)
RETURN COUNT(DISTINCT c.customerId) AS customers_with_multiple_returns

A cousin of this mechanism decided the lifetime value question: there the threshold survived translation but as a sort key, quietly turning the question into who returned the most. Multi-stage query composition, the construct a HAVING-style filter needs, is listed as not yet supported in the GQL conformance documentation, so this mechanism is partly a language gap of the preview and partly translator behavior. [6]
Mechanism three is grouping by the unique key, and this one comes with an official name. Microsoft’s troubleshooting page for ontology as a data agent source documents a known issue titled “Query results don’t aggregate correctly,” with a recommended instruction that we had applied from day one. The signature looks like this: [5]
GQL · generated by OntologyBasedAgent
RETURN r.returnId, o.orderId, s.shipmentId,
c.carrierId, c.carrierName,
COUNT(DISTINCT o.orderId) AS returned_order_count
GROUP BY return_id, order_id, shipment_id, carrier_id, carrier_name
ORDER BY returned_order_count DESC LIMIT 1

Group by a unique identifier and every group holds one row, so every count collapses to one. The agent reported the top carrier with, literally, one returned order.
Mechanism four is the fabricated constant. Asked which returned item had the highest line value, one attempt skipped the traversal to the order line entirely and projected a literal zero:
GQL · generated by OntologyBasedAgent
RETURN c.customerId, o.orderId, r.returnId,
p.productId, p.name AS product_name,
0.0d AS line_value
ORDER BY line_value DESC LIMIT 1

The answer arrived as a confident zero dollars. And the sequencing question about James Johnson belongs to mechanism one: his newer orders have no returns, the pattern reached orders only through returns, so as far as the query was concerned those orders did not exist, and the agent asserted a negative that was false.
The other half of this round’s story is why the same questions were routine for StarBasedAgent. Here is the heart of its rate query, and it reads like a textbook page because for DAX this is the textbook:
DAX · generated by StarBasedAgent for the rate question
MEASURE 'factshipment'[Orders Shipped] =
DISTINCTCOUNT ( 'factshipment'[orderId] )
MEASURE 'factshipment'[Orders With Returns] =
CALCULATE (
DISTINCTCOUNT ( 'factreturn'[orderId] ),
TREATAS ( VALUES ( 'factshipment'[orderId] ),
'factreturn'[orderId] ) )
MEASURE 'factshipment'[Return Rate %] =
DIVIDE ( [Orders With Returns], [Orders Shipped] ) * 100

Two populations, held apart and divided, exactly as the question demands. Filters, thresholds and rankings are the native grammar of this engine, which is why the same bridge technique that misfires row by row in section 5 works dependably here: applied to whole sets inside a measure, it feeds an aggregation instead of decorating a single row. Each engine is trustworthy precisely where the question speaks its language.
One note in fairness to StarBasedAgent’s near sweep here: its own record was not spotless. On the lifetime value question, one attempt decorated a correct answer with invented names that supposedly tied the winner. They did not exist. Wrong answers with confident delivery are not exclusive to either engine.
6.2 Verdict
Hypothesis two is rejected, and in the opposite direction from the popular claim. On the aggregation half of a real workload the preview ontology agent failed every question, usually every time, and always with a plausible-sounding number attached. The same model, the same data and the same questions were routine for StarBasedAgent.

Two rounds, two clean sweeps in opposite directions: each agent is excellent precisely where the other is unreliable. That symmetry converts an abstract comparison into a concrete deployment problem, because no real user base restricts itself to one family of questions. The support lead and the operations manager share the same chat window. The next section is what happened when we stopped choosing between the two models and gave one agent both.
7. One Agent over Both Models: MasterAgent
Fabric’s answer to the dilemma is hiding in plain sight: a data agent accepts up to five sources, and nothing stops the ontology and the semantic model from being two of them. We created MasterAgent with both sources attached, and put all of its guidance in one carefully constructed agent instruction: which source serves which shape of question, the shared business definitions, a fallback rule that retries the other source when a result comes back blank or degenerate, and a rule that forbids merging records when a name matches several people. The same preparation discipline from section 4 applied here doubled: this instruction was written, tested and revised with the same care as the models themselves, because with two sources present, the instruction is the router. [2] [8]


7.1 The Same Questions, One Agent
Every question from sections 5 and 6 went back in, and the results are simplest to show next to the routing decisions:
| Question class | Source MasterAgent chose | Outcome |
|---|---|---|
| All five traversal questions (section 5) | Ontology, every question, every time | All correct, including every question StarBasedAgent had failed |
| All six aggregation and ranking questions (section 6) | Semantic model, every question, every time | All correct, including every question OntologyBasedAgent had failed |
| Repeat of the same question across separate attempts | Same source every time | Stable routing, stable answers |
That table is the headline: every question class that consistently broke one source was answered correctly by routing it to the other. The instruction-based routing never picked the wrong source across the entire replay.
Two examples make it concrete, both questions the reader has already watched fail on the wrong engine. First, the Karen Thompson question, which produced a phantom carrier in section 5. MasterAgent routed it to the ontology and generated a stored walk:
GQL · generated by MasterAgent after routing
MATCH (o:Order)-[:placedBy]->(c:Customer),
(s:Shipment)-[:fulfills]->(o),
(s)-[:carriedBy]->(car:Carrier),
(r:Return)-[:returnedFor]->(o),
(r)-[:returns]->(p:Product)
WHERE LOWER(c.name) = LOWER("Karen Thompson")
RETURN p.name, r.reason, car.carrierName
-- both items, both reasons, both true carriers
Second, the carrier rate question, which collapsed to 100 percent everywhere in section 6. MasterAgent routed it to the semantic model and generated the two populations a rate needs:
DAX · generated by MasterAgent after routing
MEASURE 'dimcarrier'[Orders Shipped] =
COUNTROWS ( VALUES ( 'factshipment'[orderId] ) )
MEASURE 'dimcarrier'[Orders With Return] =
COUNTROWS ( INTERSECT (
VALUES ( 'factshipment'[orderId] ),
VALUES ( 'factreturn'[orderId] ) ) )
MEASURE 'dimcarrier'[Return Rate %] =
DIVIDE ( [Orders With Return], [Orders Shipped] ) * 100
-- the exact fifteen carrier table, NovaCold leading at 26.9 percent
Both queries are, shape for shape, what the specialists generate on their home ground. Routing changed who answered, not how; MasterAgent’s contribution is choosing the engine whose language the question speaks.

7.2 The Question Neither Source Can Answer Alone
So far every question has belonged to one family or the other. What happens when a single question needs both engines at once? We built one to find out: name the customer with the highest lifetime value among those who have made more than one return, then list what that customer returned and which carrier shipped each of those orders. The first half is an aggregation with a threshold, the exact shape section 6 showed the ontology failing. The second half is a traversal, the exact shape section 5 showed the star failing. Neither specialist can win both halves, and when we put the question to them, neither did:
| The two halves of one question | Engine it needs | OntologyBasedAgent | StarBasedAgent |
|---|---|---|---|
| Who has the highest lifetime value among customers with more than one return? | Set logic (semantic model) | Fail Named the wrong customer; the threshold collapsed as in section 6 | Pass Exact on both the customer and the value |
| What did that customer return, and which carrier shipped each order? | Path logic (ontology) | Fail Traced the wrong customer’s returns | Fail Every order attributed to the phantom carrier of section 5 |

A two-source agent needs an explicit rule for questions like this, because routing alone commits to one source, and one source cannot carry both halves. The practice that works is to write the split into the agent instruction: when a question contains both shapes, never answer it from a single source; resolve whichever half the other depends on first, carry the entity’s id rather than its display name across the boundary, and combine the results. With that rule in its instruction, MasterAgent answers in two clean steps, and the generated queries are worth showing because they are this entire article in miniature. Step one, the semantic model doing what set logic does best:
DAX · generated by MasterAgent, step one
VAR _CustomerReturnStats =
SUMMARIZECOLUMNS (
'dimcustomer'[customerId],
'dimcustomer'[customerName],
"Return Count", COUNTROWS ( 'factreturn' ),
"Lifetime Value", MAX ( 'dimcustomer'[lifetimeValue] ) )
VAR _MultiReturnCustomers =
FILTER ( _CustomerReturnStats, [Return Count] > 1 )
VAR _TopCustomer =
TOPN ( 1, _MultiReturnCustomers, [Lifetime Value], DESC )
-- result: the top customer's id, carried into step two
This is the shape DAX exists for: build a set, filter it on its own aggregate, rank what remains. Step two, the ontology walking named edges from the id that step one produced, trimmed here to the essential pattern:
GQL · generated by MasterAgent, step two
MATCH (o:Order)-[:placedBy]->(c:Customer),
(r:Return)-[:returnedFor]->(o),
(r)-[:returns]->(p:Product),
(s:Shipment)-[:fulfills]->(o),
(s)-[:carriedBy]->(car:Carrier)
WHERE LOWER(c.customerId) = LOWER(<the id from step one>)
RETURN o.orderId, p.name AS product_name,
car.carrierName AS carrier_name
-- result: each returned item with its true carrier
The final answer was exact on every field. The id handoff is the detail that makes it work: several customers in this dataset share the winning customer’s display name, some of them with returns of their own, and none of those leaked into the answer because the second step filtered on the id, not the name. The threshold and the ranking ran in the engine built for sets; the walk to carriers ran in the engine built for paths; and the seam between them was one paragraph of instruction.
A second behavior deserves its own mention: decomposition. The sequencing question from section 6, the one OntologyBasedAgent fails as a single query, was routed by MasterAgent to the ontology anyway, but as two simple queries: find the most recent return date, then count orders after that date. Both steps sit comfortably inside GQL’s strengths, and the combined answer was exact. The orchestrator can rescue its own engine from a known weakness by splitting the work.
7.3 Guardrails under Pressure
Two stress scenarios probed behavior rather than correctness, and both point at instruction patterns worth treating as standard practice for any production agent. Real data does not keep names unique, so the instruction carries a mandatory pre-answer check: when a name matches more than one record, never merge them into one answer; report how many records match and answer per customer, distinguished by region and email. Asked about a customer name that several people share, MasterAgent did exactly that. The instruction also defines the agent’s scope and how to refuse outside it, and asked something the data cannot answer at all, a headcount question, MasterAgent declined with a named reason and fabricated nothing.
7.4 The Stress Test: When the Two Shapes Fuse into One Step
One question remained: what happens when a question cannot be split at all? section 7.2’s question was hard but separable; its halves could be answered one after the other and joined by an id. So we engineered a question designed to corner the orchestrator: among customers with more than one return, how many had at least one returned order shipped by a specific carrier. The ontology’s weak half, a threshold on an aggregate, and the star’s weak half, a hop across facts, fuse here into a single count over the whole customer base. There is no “find X, then trace X,” nothing to chain, and the splitting rule cannot apply. The locked answer is 1,398 of 2,652.
Facing a question it cannot divide, the router has one move left: hand the whole thing to a single engine. It chose the semantic model, every time, and the choice is correct, because a fused count over the dataset is set-shaped no matter what traversal it contains. That is also why the trap mostly failed to spring. At set level the cross-fact hop runs in filter context, the star’s dependable mode and the reason it swept section 6; the per-row fragility of section 5 never enters the picture. The generated DAX from a passing attempt resolves the whole question as one set computation:
DAX · generated by MasterAgent, passing attempt
VAR _MultiReturnCustomers =
FILTER (
SUMMARIZECOLUMNS (
'dimcustomer'[customerId],
"Return Count", COUNTROWS ( 'factreturn' ) ),
[Return Count] > 1 )
-- their returned orders, intersected with NovaCold shipments,
-- counted per customer
-- result: 1,398 of 2,652
One attempt in the program went the other way, and the failing query is worth as much as the passing one:
DAX · generated by MasterAgent, failing attempt
VAR _CustomersMultiReturn =
FILTER (
ADDCOLUMNS (
SUMMARIZE ( 'factreturn', 'dimcustomer'[customerId] ),
"Return Count", DISTINCTCOUNT ( 'factreturn'[returnId] ) ),
[Return Count] > 1 )
-- evaluated without context transition, the count sees ALL returns,
-- every customer clears the threshold, the filter silently does nothing
-- result: 6,229
That number deserves a moment of respect. It is not blank, not zero, not 100 percent. No fallback rule recognizes it as degenerate, no instruction flags it, and no reader without the ground truth would blink. A plausible wrong answer survives every guardrail an agent configuration can carry, and this holds for every agent in this study.
A plausible wrong answer survives every guardrail an agent configuration can carry. The only defense is an answer key the agent never sees.
| Question | StarBasedAgent | OntologyBasedAgent | MasterAgent |
|---|---|---|---|
| Mark Hill: returns and carriers | Fail | Pass | Pass |
| Karen Thompson: returns and carriers | Inconsistent | Pass | Pass |
| Michelle Torres: delayed item, carrier, reason | Fail | Pass | Pass |
| Mary Miller: delayed item, carrier, reason | Inconsistent | Pass | Pass |
| Nancy Lopez: returns and warehouses | Fail | Pass | Pass |
| Return rate per carrier, highest | Pass | Fail | Pass |
| Quarter with most returned items | Pass | Fail | Pass |
| Customers with more than one return | Pass | Fail | Pass |
| Highest lifetime value among repeat returners | Inconsistent | Inconsistent | Pass |
| Orders placed since most recent return | Pass | Fail | Pass |
| Carriers with return rate above 20 percent | Pass | Fail | Pass |
| Mixed question needing both sources (7.2) | Fail | Fail | Pass |
| The boundary case (section 8) | Inconsistent | Inconsistent | Inconsistent |
7.5 Operational Considerations
Coverage has a price, and it is worth stating precisely because the numbers are not dramatic in the way one might guess. Measured across the program, MasterAgent’s average response time was roughly two and a half times OntologyBasedAgent’s and moderately above StarBasedAgent’s, with a longer worst-case tail; one attempt during a heavy testing stretch was throttled by capacity on both sources and recovered on retry. Fabric’s documentation also notes that conversation history can influence answers, and with two sources attached we saw one clear case of a question inheriting an entity from an earlier question in the same session. For evaluation work, one question per session is the discipline; for production, expect multi-turn conversations to be messier than your test bench. [1]

7.6 Verdict
MasterAgent did what neither specialist could: it covered the full question set, chained its sources across a question that spans both worlds once its instructions told it how to split such questions, split a hard question into easy ones entirely on its own, held its routing steady, and refused cleanly at the boundary of its knowledge. It remains an orchestrator over two imperfect engines, so it inherits whichever dice it routes to, and it answers more slowly than either specialist. As a deployment posture, that trade is easy to defend. As a guarantee of correctness, it is not one, and 7.4 and section 8 show why no configuration can be.
8. The Question That Refused to Settle
One question in the program would not give any agent a clean record: for each item a customer returned, was the shipment delayed, and what was the item’s line value. It reads like an ordinary traversal question, and its delay half is one. We tested it with four different customers as the anchor, on all three agents, repeatedly. No agent passed it every time, no agent failed it every time, and the failures did not stay attached to any particular customer; an anchor that one agent answered perfectly in one attempt produced a wrong answer in another.
| Customer asked about | StarBasedAgent | OntologyBasedAgent | MasterAgent |
|---|---|---|---|
| Patricia Anderson | Fail Delay flags reported on time (two shipments were late), values missing, and the agent suggested the data was incomplete | Inconsistent Exact in some attempts; eleven line values instead of three in others | Fail Inherited the leak: the whole order’s lines presented as returns |
| Michelle Torres | Inconsistent Exact in some attempts; wrong flags and missing values in others | Inconsistent Exact in some attempts; items she never returned in others | Inconsistent Exact in some attempts; the same leak in others |
| Mary Miller | Inconsistent Exact in some attempts; every delay flag wrong in others | Inconsistent Exact in some attempts; dozens of phantom returns in others | Pass Exact |
| John Flores | Fail Delay flags wrong | Pass Exact | Inconsistent Exact in some attempts; values summed across the whole order in others |
8.1 Reading the Failures
The delay half of the question almost never caused the trouble. The money half did. The line value lives on the order line, and to fetch the right one, a query must match on two things at once: the same order and the same product as the return. Every correct answer in the table above came from a query that held both conditions. Every wrong answer came from a query that dropped one.

The generated queries show the drop plainly on both sides. When StarBasedAgent misses, its bridge between the return and sales facts silently returns nothing, so values render blank and delay flags default to on time:
DAX · generated by StarBasedAgent
"Line Value USD",
CALCULATE (
SUM ( 'factsales'[lineTotal] ),
TREATAS (
ROW ( "orderId", _OrderId,
"productId", _ProductId ),
'factsales'[orderId],
'factsales'[productId] ) )
-- evaluated per returned row: came back blank in the failing attempts
When OntologyBasedAgent misses, its pattern reaches the order’s lines without pinning them to the returned product, so every line of the order leaks into the answer:
GQL · generated by OntologyBasedAgent for a returned item value question
MATCH (ol:OrderLine)-[:belongsTo]->(o:Order),
(r:Return)-[:returnedFor]->(o),
(r)-[:returns]->(p:Product)
-- the missing leg: (ol)-[:references]->(p)
-- without it, every line of the order pairs with the return
The leak even does arithmetic. In one MasterAgent attempt the Cabin Filter’s value came back as $1,336.01, which is exactly $1,085.55 plus $250.46: the two lines of that order added together, just as figure 18 shows. MasterAgent routed this question correctly every single time; what varied was the engine underneath, because routing cannot repair a query the engine improvises.
Set this against the rest of the program and the pattern is a controlled comparison we did not have to stage. Wherever the model stored a path, the ontology agent answered consistently, every anchor, every attempt. The one path it could not store produced the only traversal behavior that varied. Same agent, same data, same protocol; the only difference is whether the connection exists in the model.
8.2 Why the Path Cannot Exist
So why not draw the missing connection? We tried. The ontology will happily define a relationship from Return to OrderLine, but binding it to data requires what every relationship requires: some table holding both sides’ identifiers in the same row. No such table exists, because the source data never recorded which line a return came from. A return knows its order and its product; when that order holds the same product on several lines, nothing anywhere says which line came back. The relationship screen ends the argument by itself: it asks for a column of order line identifiers, and no table in the dataset has one to offer.
Nor is this gap unusual. Returns data in retail systems typically records the order and the item and nothing finer, because that is all a returns process needs to operate. Reports and dashboards never notice, since totals, rates and trends do not care which line an item came back from. An agent is the first consumer of this data that asks the question at line level, which means gaps like this one sit quietly in most warehouses today, waiting for the first agent to find them.
And here the two models part ways one final time, because they sit at very different distances from a fix. For the ontology, the cure is one definition away: the moment the data records which line a return belongs to, a single relationship type binds and this question becomes an ordinary stored walk, indistinguishable from the traversals it already answers consistently. For the semantic model, no amount of data helps. A pure star schema does not permit relationships between fact tables, so even with the missing fact recorded, the connection between a return and its sales line would remain exactly what it is today: a join the translator must improvise on every question. One model’s gap is a data preparation task. The other’s is the pattern itself. [11]

8.3 What to Take from It
An agent is only as reliable as the explicit paths in the model beneath it. Where a path exists, the agent walks it. Where none exists, every answer is an improvisation.
The audit this points to is practical and bounded. No team can enumerate the questions its users will eventually ask, and none needs to: a model contains a finite catalog of business objects, and questions only ever combine them, a return with its value, an order with its carrier, a product with its warehouse. Reviewing that catalog and confirming that a stored path connects every pair the business talks about in the same sentence is a bounded exercise, and each missing path it surfaces becomes a decision made before go-live rather than a surprise after it: capture the connection during data preparation, or accept and document the variability. Once the agent is live, its own question log completes the picture. Real questions, graded against independently computed answers, form a regression suite that grows with usage, and it is the only mechanism in this entire study that caught every failure, including the ones that read perfectly.
9. Discussion
Step back from the individual verdicts and the whole picture becomes almost tidy. Two sweeps in opposite directions, and both explained by the same principle viewed from opposite sides.
GQL thinks in paths. Ask it to walk from Michelle Torres to her returns to their shipments to a carrier, and the generated query is short, readable and correct, because that is what pattern matching is. Ask it to compute a rate, and the four mechanisms of section 6 appear, because rates, thresholds and buckets are set operations that the translator keeps trying to express as paths. DAX thinks in sets. Ask it which carrier exceeds a 20 percent return rate and the generated measure is textbook. Ask it to walk from one return to one shipment, and section 5 showed what the improvisation looks like: silent blanks and alphabetical phantoms.
Every consistent failure we logged, on either side, happened in exactly one of two places: where a question crossed into the engine’s foreign territory, or where the model beneath it offered no explicit path. Nothing else broke. That regularity is good news, because it makes failure predictable before deployment: look at your users’ question shapes, look at your model’s explicit paths, and you can draw the failure map before the first user ever types a question.
The two blind spots also differ in a way worth weighing for anyone planning a year ahead. The star’s inability to traverse is a design decision of dimensional modeling itself: facts do not join facts, by rule, and no product release changes a modeling pattern. The ontology’s aggregation troubles look different in the documentation: the grouping collapse is a published known issue of a preview feature, the missing multi-stage composition is listed as not yet supported, and OPTIONAL MATCH already exists but goes unused by the translator. Documented known issues in a preview have a habit of shrinking with releases. We promise nothing on Microsoft’s behalf, and two behaviors we observed, fabricated constants and misread result tables, are model behaviors no language feature will fix. But it is a fair reading that one of these blind spots can close over time and the other cannot. [5] [6] [11]
10. What These Results Point To
The findings convert into a deployment decision more cleanly than most benchmarks do.
| If the workload is | Ground the agent in | Because |
|---|---|---|
| Investigations and traces: what happened with this order, customer, return | The ontology | Named relationships make every hop explicit. StarBasedAgent fabricated entities or blamed the data on these questions. |
| Rates, rankings, thresholds, time buckets, sequences | The semantic model | Set operations are DAX’s native ground. OntologyBasedAgent failed every question of this family during testing. |
| Both families, which describes every real deployment we have seen | One agent holding both sources with routing instructions | MasterAgent answered every class either specialist failed, and the orchestrator added chaining and decomposition on its own. |
Three practices should travel with whichever row applies. Write routing instructions by question shape rather than by table name, and include the fallback retry, the disambiguation check, and the splitting rule for questions that carry both shapes; the router is only as good as this text. [10] Prepare both models with the same seriousness you would give a report used by the board, because agent answer quality tracked documentation quality throughout our testing. And before anyone trusts the agent, build the answer key: a set of questions with independently computed answers, kept current and re-run on every change. As section 8.3 showed, this is what catches the failures that look perfectly plausible.
11. Conclusion
Is an ontology-powered data agent a step up from a semantic model? On the questions ontologies were built for, our testing says emphatically yes: OntologyBasedAgent turned question shapes that StarBasedAgent could not reliably survive into routine, correct answers, and the difference is architectural. The claim in our title survives its test.
It survives narrowly, though. The same preview agent that swept the traversal questions failed every aggregation and ranking question we asked it, and those are the questions most teams ask most often. Anyone deploying an ontology agent today as their only grounding is trading one blind spot for a larger one.
The good news is that the trade is unnecessary. Both shapes of the same data can stand behind one agent, the routing between them is a page of carefully written instructions, and in our testing that combination answered everything either specialist could answer plus the questions neither could. What it cannot do, and what nothing in this article can do, is certify its own answers. The one failure that defeated every agent and every guardrail was the wrong answer that looked right. Against that, the defense is an answer key the agent never sees, and the habit of using it.
An agent is only as reliable as the explicit paths in the model beneath it. That sentence is what we came away with, it predicted every failure we observed on both sides of this comparison, and it is the first thing we now check before letting any agent near a user.
If you are deciding how to ground Fabric data agents in your own models, Data Crafters has tested both approaches side by side. Let’s talk about what it looks like for yours.
References
- Fabric data agent concepts. Microsoft Learn. learn.microsoft.com/fabric/data-science/concept-data-agent
- Create a Fabric data agent. Microsoft Learn. learn.microsoft.com/fabric/data-science/how-to-create-data-agent
- Add and configure data sources in Fabric data agent. Microsoft Learn. learn.microsoft.com/fabric/data-science/data-agent-add-datasources
- What is ontology (preview)? Microsoft Learn. learn.microsoft.com/fabric/iq/ontology/overview
- Troubleshooting ontology (preview). Microsoft Learn. learn.microsoft.com/fabric/iq/ontology/resources-troubleshooting
- GQL standard conformance for graph in Microsoft Fabric. Microsoft Learn. learn.microsoft.com/fabric/graph/gql-conformance
- GQL language guide for graph in Microsoft Fabric. Microsoft Learn. learn.microsoft.com/fabric/graph/gql-language-guide
- Improve data source routing in a Fabric data agent. Microsoft Learn. learn.microsoft.com/fabric/data-science/data-agent-routing
- Semantic model best practices for data agent. Microsoft Learn. learn.microsoft.com/fabric/data-science/semantic-model-best-practices
- Best practices for configuring your data agent. Microsoft Learn. learn.microsoft.com/fabric/data-science/data-agent-configuration-best-practices
- Understand star schema and the importance for Power BI. Microsoft Learn. learn.microsoft.com/power-bi/guidance/star-schema
















































