Why Your Fabric Data Agent Gives Wrong Answers (And How Data Enrichment Fixes It)
PowerBI consultant ·
Everyone wants conversational AI on their data now — ask a question, get a number back, skip the DAX and the SQL and the three-day wait for someone to build a report. Fabric Data Agent actually pulls this off. Point it at a semantic model, a lakehouse, a warehouse, a KQL database, and it answers questions typed in plain English, and it's honestly one of the only AI features in this whole ecosystem that's lived up to its own demo.
It's only as good as your metadata though, and I found that out the hard way enriching a model for a Data Agent rollout. It kept whiffing on questions that should've been trivial — same table, same data, and depending on how you phrased the question you'd get a wrong answer or nothing back at all. Took some digging to see why. The fix, once I actually saw what was going on, seemed almost stupidly simple. It's not, not really, not once you're doing it across a whole real model instead of a couple of test columns, but that's later in this post.
What You Need Before You Start
Nothing exotic — just check these first:
A Fabric workspace on F2 capacity or higher. Data agents don't run on a trial tier or a plain Pro workspace.
Power BI Desktop with TMDL view (GA since September 2025), if you want to edit metadata as text instead of clicking through Model view field by field.
Tabular Editor 2 or 3, if you're scripting anything in bulk — the C# example later assumes Advanced Scripting.
At least one lakehouse, warehouse, semantic model, or KQL database already published. A single agent connects to up to five sources.
Skip all this if you're just here for the concepts. If you're actually building one: the capacity tier is what trips people up, and Microsoft's docs bury it.
How a Data Agent Actually Answers a Question
From the chat window it feels like magic. Under the hood it's a lot more mechanical, almost disappointingly so once you've seen it a few times: someone types a question, the engine parses it and tries to match the words against objects in your model, then converts the whole thing into a native query — DAX for a semantic model, SQL for a warehouse, KQL for a database. That query runs and the answer comes back.
Matching words to the right column is the step that decides whether you get a correct answer, and it's the one I think gets the least attention in Microsoft's own materials. Take a Sales table with a column called Revenue. Ask what the Net Revenue is, and you get a clean match — correct number back. Ask what the Net Sales is instead, and unless the model has been told those two phrases mean the same column, there's nothing to match against. Wrong number, or no answer, and that's not really the AI failing so much as a gap nobody filled in — though I get why it feels like the AI's fault when you're the one staring at the wrong number.
Closing the Gap: Data Enrichment
Enrichment closes that gap. It means layering business context onto a model — descriptions and synonyms on tables, columns, and measures — so the Data Agent knows not just what a field is named, but what it means and what else someone might call it.
Descriptions and synonyms get lumped together a lot. I used to lump them together myself. They're not the same thing, they live in different parts of the model and solve different problems, and treating them as interchangeable is exactly how people end up confused later.
A description is a standard metadata property, sitting at the same level as a table or column name — plain text, shows up as a tooltip in the field list, and because it's a first-class TMDL property it's trivial to script or bulk-edit. Honestly this is the easy chunk of enrichment. Shouldn't eat more than an afternoon of anyone's time.
Synonyms are where it actually gets annoying. They belong to the model's Linguistic Schema, the layer that powers Q&A and natural-language querying, and a synonym only exists inside a specific model culture — en-US, say. Skip that step and there's nowhere for the synonym to live, which is probably the single most common mistake people make with this feature, and I think it's kind of a design flaw that Microsoft doesn't surface this dependency more clearly in the UI. You won't find synonyms sitting next to the description property in TMDL either, they live in a completely separate collection, keyed by culture. It's fine once you know it's there, but the first time you go looking for a synonym in the same place as a description and it's just not there, it's genuinely confusing.
Adding a Synonym or Description (The Easy Way)
Model view is the quickest path, and for a handful of fields I'd actually recommend just using it — no reason to overengineer three descriptions with a script. Select a table or column, and the Properties pane puts both fields side by side.
Type a description, drop a few alternate terms into Synonyms, done — the whole UI-driven workflow in about ten seconds. Fine for a handful of fields. Past that it gets tedious fast, and I gave up doing it this way somewhere around thirty columns in.
TMDL view does the same job as a comment instead of a form field, and this is where I actually prefer working. A /// line sits directly above whatever it documents — here, right above table RiskRegister:
Columns and measures follow the same rule: the comment goes directly above the object it's describing. That predictability is what makes descriptions so easy to bulk-generate — one line of text per object, not a nested structure to build by hand. Synonyms don't get that same courtesy.
What Actually Happens Behind the Scenes
This is where the two features really pull apart, and it's where most people's idea of "enrichment" quietly falls apart on them too. Add a synonym to anything for the first time, and Power BI creates a linguistic metadata block behind the scenes, nested under a cultureInfo tag.
That block is structured JSON — a Version, a Language, an Entities collection mapping each enriched object to its terms. Each term carries a State, whether it was typed by a person or suggested by the engine, and for suggested terms, a Source and a confidence Weight. Enrich one field and you can watch this happen live, the built-in thesaurus proposing a handful of related terms on its own, which is kind of fun to watch the first time — less fun the fifth time, when you're just trying to figure out why some suggested term with a low weight isn't actually helping anything.
This is the part that changes your approach if you're planning to enrich at scale. Descriptions are flat and per-object, so looping through hundreds of them is trivial. Synonyms need a culture to exist first, and the moment you're editing linguistic metadata directly — TMDL, or Tabular Editor's C# scripting — you're working with nested JSON per culture, not a simple key-value pair. Plan the automation accordingly. And don't believe anyone who tells you synonyms are basically the same as descriptions, just for Q&A — I've heard people say that more than once and it's just not true.
Common Pitfalls
A few ways this goes wrong in practice — mistakes I made myself, or watched someone else make. Some of these annoy me more than others, which will probably show.
The quietest one, and my least favorite: no culture, no synonyms, and nothing tells you. Add a synonym, the UI looks completely normal, save goes through fine, but Q&A still can't resolve the term because there was never a culture block for it to live in. Check for cultureInfo before assuming synonyms are doing anything at all. This should really throw some kind of warning and it just doesn't.
Bulk scripts are another, and this one's on the person running the script, not the tool. Run a description-generation script without checking for existing values first, and every carefully hand-written description gets flattened back down to a generic placeholder. It's exactly why the script further down checks before it writes — that happened to a model I was working on, and undoing it wasn't fun.
Then there's testing only the easy phrasing, which I'm guilty of too — it's tempting to declare victory the moment "What's the Net Revenue?" works. The whole point of enrichment is the awkward phrasing though, that's what actually needs testing, not the version that was never going to fail.
One more that catches people, and this is more of an opinion than a technical gotcha: treating Q&A and Copilot's Ask Data as permanently identical. They share an engine today, but Microsoft has Q&A on a retirement path, which is probably the right call honestly — the classic Q&A visual never really worked the way it was supposed to. Enrichment still matters for how Copilot works right now. Just don't build a long-term strategy around classic Q&A visuals sticking around.
And the slow one, the one that shows up latest: enriching once and calling it done. Usually surfaces a few months after launch, when someone adds a new column and nobody remembers there was ever a step for describing it. This one's probably the hardest to actually fix, because it's not really a technical problem, it's a process problem, and those don't fix themselves.
Next Steps
Here's the order I'd do this in:
Check for a defined culture before anything else. No cultureInfo block, no home for synonyms.
Don't enrich the whole model in one sitting. Find out what people actually ask, and go after the terms most likely to collide first — Revenue and Sales, Customer and Client.
Typing /// comments one column at a time works for a handful of fields, then falls apart. Don't be a hero about it — a short C# script through Tabular Editor's Advanced Scripting window scales this properly: set Description on the TOM model, save, and Tabular Editor writes the /// TMDL back out on its own. You never touch it by hand.
Here are two patterns to start from — neither is fancy, and they don't need to be. The first auto-generates a placeholder for every column missing a description and gives you an inline dictionary for dropping in real business wording where it actually counts. The second reads that same Table.Column → Description mapping from a TSV file instead of a hardcoded dictionary, which is the one worth reaching for once the definitions are coming from someone other than you:
// Tabular Editor 2/3 — Advanced Scripting (C#) // Bulk-seeds column descriptions across the whole model. // Run this, then refine the auto-generated placeholders by hand // (or feed the model.Tables into an AI enrichment pass). // Bulk-seeds column descriptions across the whole model. // Known business terms map var knownDescriptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "RiskRegister.Risk ID", "Unique identifier for a logged risk item." }, { "AlertLog.Domain ID", "Gross revenue before discounts and returns." }, { "AlertLog.Description", "Revenue after discounts, returns, and allowances." } }; int updated = 0; int skippedExisting = 0; int skippedNoMatch = 0; foreach (var table in Model.Tables) { foreach (var column in table.Columns) { string fullKey = table.Name + "." + column.Name; string colKey = column.Name; string desc = null; // Check Table.Column first, then fall back to Column if (knownDescriptions.TryGetValue(fullKey, out desc) || knownDescriptions.TryGetValue(colKey, out desc)) { column.Description = desc; updated++; } else { skippedNoMatch++; } } } // Display summary box in Tabular Editor UI string summary = "Description Seeding Complete:\n\n" + "• Updated: {"+updated+"}\n" + "• Skipped (Already described): {"+skippedExisting+"}\n" + "• Skipped (No match in map): {"+skippedNoMatch+"}"; Info(summary);That inline dictionary is fine for three or four descriptions typed straight into the script, but that's rarely how the real definitions show up — they come from a data dictionary, a subject-matter expert's spreadsheet, or an export someone already has sitting in a file. This second script does the same job, except it reads the Table.Column → Description mapping from a plain TSV file on disk instead of a dictionary hardcoded into the C#, so someone else can maintain the source file without ever opening Tabular Editor, and a malformed row in the file gets counted and skipped instead of silently breaking the run:
// ==== CONFIG ==== string filePath = @"C:\temp\ColumnDescriptions.tsv"; // Expected TSV format (tab-separated), one entry per line, no header: // Table.Column<TAB>Description // e.g. // RiskRegister.Risk ID<TAB>Unique identifier for a logged risk item. // AlertLog.Domain ID<TAB>Gross revenue before discounts and returns. // AlertLog.Description<TAB>Revenue after discounts, returns, and allowances. // You can also use just "ColumnName" (without table prefix) as a fallback key. if (!System.IO.File.Exists(filePath)) { Error("File not found: " + filePath); return; } var knownDescriptions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); int malformedLines = 0; foreach (var line in System.IO.File.ReadAllLines(filePath)) { if (string.IsNullOrWhiteSpace(line)) continue; var parts = line.Split('\t'); if (parts.Length < 2) { malformedLines++; continue; } string key = parts[0].Trim(); string description = parts[1].Trim(); if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(description)) { knownDescriptions[key] = description; // last value wins on duplicate keys } else { malformedLines++; } } int updated = 0; int skippedExisting = 0; int skippedNoMatch = 0; foreach (var table in Model.Tables) { foreach (var column in table.Columns) { string fullKey = table.Name + "." + column.Name; string colKey = column.Name; string desc = null; // Check Table.Column first, then fall back to Column if (knownDescriptions.TryGetValue(fullKey, out desc) || knownDescriptions.TryGetValue(colKey, out desc)) { column.Description = desc; updated++; } else { skippedNoMatch++; } } } // Display summary box in Tabular Editor UI string summary = "Description Seeding Complete:\n\n" + "• Source file: " + filePath + "\n" + "• Entries loaded: " + knownDescriptions.Count + "\n" + "• Malformed lines skipped: " + malformedLines + "\n" + "• Updated: " + updated + "\n" + "• Skipped (Already described): " + skippedExisting + "\n" + "• Skipped (No match in map): " + skippedNoMatch; Info(summary);A couple of things about how these scripts behave: both use the same table.column-first-then-column-name lookup, so a bare column name in your source acts as a fallback for tables where you haven't bothered qualifying it. The dictionary version is the quicker one to reach for on a handful of fields. The TSV version is the one I actually use on a real model — the mapping lives in a file instead of inside the script itself, so a subject-matter expert can edit ColumnDescriptions.tsv directly, and malformed lines get reported in the summary instead of failing quietly. Swap Columns for Measures in either loop and the same logic handles measure descriptions too. Some people are going to want to skip the dictionary or the file entirely and fully automate this with an LLM pass. I'd be careful with that — I've seen auto-generated descriptions that sounded completely confident and were just wrong, and that's honestly worse than having no description there at all.
Test with real questions, not just the ones you already know will work. Ask for "Net Sales," not just "Net Revenue," and confirm the agent resolves it before calling the job finished.
Revisit this. New columns don't inherit descriptions or synonyms just because older ones had them — build that into the change process, or it quietly rots the moment nobody's watching.
Conclusion
A Data Agent is only as fluent as the metadata behind it. Descriptions and synonyms look like a minor, almost cosmetic addition to a model, but they're the actual difference between an AI that understands your business language and one that quietly gives wrong answers to reasonable questions. I didn't take this seriously enough the first time I built a model for a Data Agent, figured the AI would just "figure it out." It doesn't, not reliably, and I found that out the annoying way. Once you know where each piece lives — a flat property for one, a culture-scoped schema for the other — enriching a model stops being a mystery and turns into ordinary, scriptable work. I still think Microsoft could make the synonym side of this less fiddly than it is. Until they actually do, this is what you're stuck working around.
Further Reading
Primary sources, if you want more than my take on it:
Fabric data agent overview: Microsoft's core reference for what a data agent is and how it connects to sources.
Data agent configurations — instructions, few-shot examples, and other ways to steer responses beyond enrichment alone.
TMDL overview: the full object model and syntax reference.
Use TMDL view in Power BI Desktop — how to script, preview, and apply TMDL changes directly in Desktop.
Tabular Editor Useful Script Snippets: collection of C# scripts for updating power bi model properties.
Keep reading
PL-300 Episode 3: Design the Data Model
PL-300 Episode 3: star, snowflake and galaxy schemas, properties, relationships, role-playing dimensions, date tables, calculated columns and tables, with M and DAX.
From Connection to Confidence: Getting, Connecting & Profiling Data in Power BI
Microsoft Data Days 2026 | PL-300 Certification Series — Part 01