I had a customer reach out for help recently. Their issue? Their Power BI report developer built a report based on their semantic model, but created many, and I mean many, local measures inside the report. The measures were built around the business logic and needs, and other report developers also wanted to use them in different reports. So, our customer asked: Is there a way to do this with Tabular Editor? My answer: There isn’t anything built in, but a C# script should be able to do this for you.
And then I wondered if I could write such a C# script? My initial answer was no; I am not a C# developer. I dabble and know enough to get into trouble, but this was probably a bridge too far for me. I would probably have asked Daniel in the past, but this being the age of AI, I wondered if I could use an LLM to augment my own meagre C# skills. Turns out, of course, it can.
Do you just want the script? Go to the final script section to get it.
A common best practice is to create a central semantic model and live connect to this model in Power BI when creating new reports. This is a way to centralize and store one version of the truth for your model, measures, and dimensions to ensure all reports show the same data. After all, this is one of the central tenets of Business Intelligence.
Additionally, Power BI allows the report creator to create local measures in the live connected reports, a great feature that ensures the report developer can create report-specific measures, building on the foundation of the central semantic model. This is, for example, very useful for creating custom visuals using SVG measures.
One big issue is that recreating these measures in your central semantic model is a painfully manual process. The no-code option would look like this:
This process is all right if you have only one measure to copy, but it gets tedious when you want to move more than a couple. That is where C# scripting comes in: to avoid manual work (efficient, not lazy), do tasks faster, make the process repeatable, and get started. It is always good to understand the context yourself first.
The first context needed is knowing where the local measures are stored inside a Power BI report. To do this, I turned on the Public Preview of the .pbip format inside Power BI Desktop as well as enabled the preview of the report folder JSON structure (.pbir). Once these prerequisites were set up, all that was needed was an actual live connected report with local measures to locate – which I then did.
The .pbip and .pbir formats are in public preview. Make sure to keep a backup of your report if trying the script out at home.
It would probably have been possible to have the AI step in right here and point it to the PBIP folder and have it search through for the measures, however I decided that taking the time to understand the requirements of my script before having the AI assist would be a worthwhile effort.
Luckily, this specific use case is mentioned in the docs, in that it directly says that the reportExtensions.json file in the definition\ folder contains report-level measures. So we have the context and are good to go.
I used ChatGPT 04-mini-high to create this script. As I can’t share chats publicly, I have provided a summary of the interaction at the bottom of the article, which I had ChatGPT create.
Additionally, I used custom instructions for the ChatGPT project, which can also be found at the bottom of the article.
For my first prompt, I attached a zip file of the PBIP folder to let the LLM know the context of where I wanted to find the measures, but otherwise provided a relatively sparse prompt:
I need to create a script for accessing local measures in a Power BI PBIP folder (an example of a PBIP folder is attached). The file that needs to be accessed is reportExtensions.json that contains an array of entities, each is a measure that needs to be moved into a model inside Tabular Editor.
And voila, it created a script. Did it work? No!
// Script: Extract local measures from a PBIP folder's reportExtensions.json and create them in the model
// Step 1: Define the path to the reportExtensions.json (adjust this path as needed)
string pbipFolderPath = @"C:\\Path\\To\\PBIP";
string reportExtensionsPath = Path.Combine(pbipFolderPath, "Report", "reportExtensions.json");
// Step 2: Read and parse the JSON file
if (!File.Exists(reportExtensionsPath))
throw new Exception($"File not found: {reportExtensionsPath}");
string json = File.ReadAllText(reportExtensionsPath);
dynamic reportExtensions = JsonConvert.DeserializeObject(json);
// Step 3: Loop through each entity and create a measure in the model
foreach (var entity in reportExtensions.entities)
{
string expression = entity.expression;
string name = entity.name;
string table = entity.table; // Optional, some might be null or implicit
// Find or create the table to host the measure
var targetTable = Model.Tables.Find(table) ?? Model.AddTable(table);
// Add the measure to the model
if (!targetTable.Measures.Contains(name))
{
var measure = targetTable.AddMeasure(name, expression);
measure.FormatString = "General";
measure.DisplayFolder = "Imported Measures"; // Optional customization
}
}
// Step 4: Notify completion
Output("Local measures imported successfully from reportExtensions.json.");
There are both compilation errors, logical errors, as well as minor hallucinations of Tabular Editor functions that do not work. But it is a starting point for having the LLM iterate a new script. I then proceeded to solve one or two issues at a time by giving it instructions, e.g., by telling it the error messages that Tabular Editor reports after running the script, suggesting API documentation on the web it should look at, and giving it examples of the reportExtensions.json schema.
Here are a few tips to consider that I found helpful when working with the LLM to create this script.
reportExtensions.jsonI am sure there are many more tips and tricks out there, but these were some concrete steps I took in the process of creating this script for Tabular Editor 3.
I relatively quickly (3-4 iterations) had a working script that I could send to the customer. It still required manual copy-pasting the path for the pbip folder into the script, but it did the job, and bulk copied all local measures into the model. And it had taken me a fraction of the time it would if I had to do it myself.
However, I thought that a bit more could be done, so with the help of the LLM, I introduced UI elements such as selecting the pbip folder, a list of measures to import, and an ability to (de)select measures.
The main downside, of course, is that I did not become better at C# at all in the process. If I had to work through creating the script myself, I might have learned some C# along the way, but on the other hand, it would probably have taken me a whole day of work instead of an hour or less.
Please find the whole script for downloading here:
This section provides the script and walks through how to run it.
Prerequisites
Steps
F5).Below is an AI-generated summary of how the script was created. Provided here for full transparency:
1. Background & Initial Goal
The objective was to automate the transfer of local measures embedded in a Power BI PBIP project (stored in each report’s reportExtensions.json) into a connected Tabular Editor 3 model. Requirements that emerged during the session included:
.Report folders inside a single PBIP root.2. Key Milestones in the Conversation
| Iteration | Main change(s) | Why it was introduced |
|---|---|---|
| 1 | Basic script: read a single reportExtensions.json, create tables/measures. | Show end-to-end proof-of-concept. |
| 2 | Added using System.IO;, System.Linq;, Newtonsoft.Json. | Resolve missing namespace compile errors. |
| 3 | Replaced hard-coded path with a FolderBrowserDialog; automatically found the first .Report subfolder. | Remove manual editing; improve UX. |
| 4 | Switched to ListView + CheckedListBox UI so the user can pick measures. | Enable selective import. |
| 5 | Added search box, multi-column display (Table, Measure, Expression), column sorting. | Usability for large lists. |
| 6 | Enabled selecting multiple reportExtensions.json files (multi-root import) and showed the originating Report column. | Handle multi-report scenarios. |
| 7 | Introduced a preview pane and Overwrite-existing checkbox. | Transparency & safety |
| 8 | Refactored to a MeasureDefinition class and helper functions (CreateSearchBox, CreateListView). | Maintainability, extensibility |
| 9 | Added duplicate-detection logic (table measure key). | Initially suffixed both table & measure; later refined to suffix only the measure when duplicates exist. |
| 10 | Replaced Application.UseWaitCursor with WaitFormVisible = false; to hide the default wait form. | Better visual experience in TE3. |
| 11 | Removed corrupted duplicate code blocks that had crept in during edits, restoring compile integrity. | Final polish / compile fix. |
3. The Final Script — Feature Overview
| Feature | Implementation Details |
|---|---|
| Multi-root PBIP support | Scans every folder in the chosen root that ends with .Report\definition\reportExtensions.json. |
| Data model | MeasureDefinition class encapsulates Report, Table, Measure, Expression, and FormatString. |
| Modern UI | SearchBox with live filter; ListView columns for Measure, Expression, Table, and Report with checkboxes and sort-by-name; inline DAX preview text box; overwrite checkbox; Import and Cancel buttons. |
| Duplicate handling | Detects duplicates across reports by table and measure name. |
| Calculated table fallback | If a referenced table is missing, creates a hidden dummy calculated table (DATATABLE("Dummy", STRING, { { "" } })). |
| Overwrite logic | If a measure already exists in the target table: ; • Overwrite if the user ticked “Overwrite existing measures”. ; • Otherwise skip it. |
| No wait overlay | WaitFormVisible = false; disables the default Tabular Editor modal “Executing script” splash. |
| Extensibility | Clear separation of UI helpers and core logic simplifies further enhancements (e.g., export to CSV, multi-column sorting). |
4. Lessons Learned
MeasureDefinition) keeps the codebase readable and future-proof.5. Conclusion
The final script transforms manual, error-prone local-measure migration into a robust, user-driven workflow. By iteratively refining user feedback—path selection, preview, and conflict management—we arrived at a reproducible, professional-grade tool suitable for internal use and public demonstration.
These are the custom instructions I used to write the C# script. They are also partly created with the help of ChatGPT. Thus, all the emojis.
🧠 Assistant Instructions: C# Script Writer for Tabular Editor 3
🧾 Role
You are a C# scripting assistant specialized in Tabular Editor 3. Your job is to help users write accurate, efficient, and practical C# scripts that interact with the semantic model in Tabular Editor 3 (TE3). You assist in drafting, debugging, optimizing, and explaining scripts that automate tasks across models, tables, measures, columns, roles, relationships, and other elements.
🎯 Mission
Empower TE3 users to create and customize powerful C# scripts that automate their workflow and manipulate semantic models with precision. You use Tabular Editor’s scripting APIs and best practices as the foundation for every solution.
📘 Authoritative References
You must always consult and cite the official documentation when providing code, explanations, or guidance:
TE3 Scripting API reference: https://docs.tabulareditor.com/api/index.html
Script library for examples: https://docs.tabulareditor.com/common/CSharpScripts/csharp-script-library.html
When possible, link to relevant pages in these docs to back up explanations or recommend further reading.
🧩 Core Capabilities
You are expected to:
✅ Generate C# scripts tailored for use in Tabular Editor 3.
✅ Explain each part of the script with inline comments or descriptive summaries.
✅ Incorporate best practices, such as error handling, object validation, and script reusability.
✅ Offer script variations for different model elements (e.g., measures, columns, roles).
✅ Debug and fix errors in user-submitted scripts.
✅ Recommend enhancements (e.g., performance improvements, refactoring).
✅ Search and reference the TE3 API and script library to ensure correctness.
✅ Include all namespaces that the script needs to use in order to run.
✅ Give 4 ideas for improvements or additions to the script at the end.
🧠 Behavior Guidelines
Be precise: Validate assumptions about object types (e.g., Model.Tables, Column.DataType) using the TE3 API.
Be safe: Include null checks and confirmations before making changes to model elements.
Be helpful: When users are unsure, ask clarifying questions (e.g., what level they want to apply a script at: model, table, measure, etc.).
Be modular: When applicable, write scripts that can be easily adapted or reused.
🧪 Script Output Example (with comments)
Example: foreach(var measure in Model.AllMeasures) { Output($"{measure.Table.Name}.{measure.Name}: {measure.Expression}"); }
Reference: AllMeasures Property - TE3 API
⚠️ Limitations
🚫 Do not generate scripts unrelated to Tabular Editor or outside of the supported TOM Wrapper API.
🚫 Do not speculate on undocumented behavior—always defer to official sources.
🚫 Do not generate scripts for the Power BI service or DAX unless they’re directly embedded in TE3 scripting scenarios.
LLM AIs can be very useful for creating scripts in Tabular Editor. The example in this blog is quite straightforward with a clear path which probably makes it very well suited for LLM, but in general that is one of the strengths of C# scripting in Tabular Editor 3.
Take your semantic models further with Tabular Editor.
Give Tabular Editor a spinThe author of this article used AI assistance in the writing for accessibility reasons. The article has been edited and reviewed manually by our editors before publication.