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.
TIP
Do you just want the script? Go to the final script section to get it.
What is the "theoretical" problem?
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:
- Create the measure in the semantic model.
- Locate the local measure in the Power BI Desktop interface.
- Copy the DAX expression from the local measure.
- Paste the DAX expression into the measure in the semantic model.
- Set properties such as format strings
- Create a description for the measure.
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.
Where do local measures exist
WARNING
The .pbip and .pbir format is 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 reportextension.json file in the definition\ folder contains report-level measures. So we have the context and are good to go.
Writing a C# script with LLM
NOTE
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.
// 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 reportExtension.json schema.
Here are a few tips to consider that I found helpful when working with the LLM to create this script.
- Providing custom instructions helps the LLM understand the context.
- Giving examples – in this case, both the whole pbip folder structure and the reportextension.json
- For C# in Tabular Editor, ask the LLM to include all Using statements needed.
- Pasting in errors – If the script doesn’t compile, just paste the error and try to provide what you think is wrong, and see if that fixes the issue.
- Ask for improvements – ask the LLM for advice on how to make your script better, more user-friendly, etc.
- Provide references to Tabular Editors official API documentation as the LLM tends to hallucinate in these areas.
I 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.
Moving local measures to the semantic model script
How to use the script
Prerequisites
- Tabular Editor 3 installed.
- Power BI PBIP project(s) on disk.
Steps
- Open Tabular Editor 3 connected to your model.
- Open a the downloaded C# Script using File -> Open -> File
- Run the script (F5).
- In the folder dialog, choose your PBIP root.
- In the pop-up:
- Filter, sort and tick the measures you want.
- Optional: preview a measure’s DAX.
- Decide whether to overwrite duplicates.
- Press Import Selected Measures.
- Filter, sort and tick the measures you want.
- Confirm the imported measures appear under Imported Measures in TE3.
AI-generated summary of script creation
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:
- Support multiple .Report folders inside a single PBIP root.
- Let the user preview and choose which measures to import.
- Detect name collisions and (only when needed) append the report name as a suffix—first to tables, then later only to measures.
- Offer an overwrite existing toggle.
- Provide a smooth UI (search, sort, inline expression preview, cancel option).
- Avoid Tabular Editor’s “Executing Script – please wait…” overlay.
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, FormatString. |
Modern UI | - SearchBox with live filter. - ListView (columns Measure, Expression, Table, Report) with checkboxes and sort-by-name. - Inline DAX preview textbox. - Overwrite checkbox, Import & Cancel buttons. |
Duplicate handling | Detects duplicates across reports by `TableName |
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 TE’s default 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
- Iterative scripting inside TE3 benefits from small compile-test cycles; UI mistakes often surface as compile errors.
- Introducing a lightweight data class (MeasureDefinition) keeps the codebase readable and future-proof.
- Naming-conflict logic should be table-specific: appending only to measures preserves the semantic layer’s clarity.
- Refactoring early (helper functions) pays off when adding features like preview panes and extra buttons.
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.C# scripting assistant custom instructions
🧠 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) csharp Copy Edit // Loop through all measures and display their expressions 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. |