Writing and executing C# scripts and macros against Power BI semantic models using Tabular Editor 2/3. Automatically invoke when the user mentions "C# script", "Tabular Editor script", "TOM scripting", "MacroActions.json", "XMLA", or asks to "automate model changes", "bulk update measures", "create calculation groups", "write a macro", "format DAX expressions", "manage model metadata".
Expert guidance for writing and executing C# scripts to manipulate Power BI semantic model metadata using Tabular Editor 2/3 CLI or the Tabular Editor IDE.
Activate automatically when tasks involve:
; (semicolon required by C#)" for strings and escape with \ when needed/ in DisplayFolder paths (auto-converted to \)Info() statements for debugging - script stops at error point| Environment | C# Version | Notes |
|---|---|---|
| Tabular Editor 2 | Default compiler | Older C# syntax |
| Tabular Editor 3 | Roslyn | Supports up to C# 12 with VS2022 |
| TE2 with Roslyn | Configurable | Set in File > Preferences > General |
To use newer C# features in TE2, configure Roslyn compiler path in preferences.
Scripts automatically have these using statements applied:
using System;
using System.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;
using TabularEditor.TOMWrapper;
using TabularEditor.TOMWrapper.Utils;
using TabularEditor.UI;
These .NET assemblies are loaded by default:
System.DllSystem.Core.DllSystem.Data.DllSystem.Windows.Forms.Dll (for UI dialogs)Microsoft.Csharp.DllNewtonsoft.Json.DllTomWrapper.DllTabularEditor.ExeMicrosoft.AnalysisServices.Tabular.Dll// Assembly references must be at the very top of the file:
#r "System.IO.Compression"
#r "System.Drawing"
// Using statements come after assembly references:
using System.IO.Compression;
using System.Drawing;
| Requirement | Description |
|---|---|
| Tabular Editor 2 CLI | Download from GitHub releases |
| XMLA Read/Write | Enabled on Fabric capacity or Power BI Premium |
| Azure Service Principal | For XMLA connections (see authentication.md) |
AZURE_CLIENT_ID=<app-id>
AZURE_TENANT_ID=<tenant-id>
AZURE_CLIENT_SECRET=<secret>
# Inline script
TabularEditor.exe "WorkspaceName/ModelName" -S "Info(Model.Database.Name);"
# Script file
TabularEditor.exe "WorkspaceName/ModelName" -S "script.csx"
| Type | Format | Example |
|---|---|---|
| XMLA | workspace/model | "Sales WS/Sales Model" |
| Local BIM | path/to/model.bim | "./model.bim" |
| Local TMDL | path/to/definition/ | "./MyModel.SemanticModel/definition/" |
| PBI Desktop | localhost:PORT | "localhost:52123" |
Model ObjectAccess any object in the loaded Tabular Model:
Model // Root model object
Model.Tables // All tables
Model.Tables["Sales"] // Specific table
Model.AllMeasures // All measures across all tables
Model.AllColumns // All columns across all tables
Model.Relationships // All relationships
Model.Roles // All security roles
Model.CalculationGroups // All calculation groups
Model.Perspectives // All perspectives
Model.Cultures // All translations/cultures
Model.Expressions // All M expressions (shared queries)
Model.DataSources // All data sources
Selected ObjectAccess objects currently selected in the TOM Explorer (IDE only):
// Plural form - collections (safe even when empty)
Selected.Tables // Selected tables
Selected.Measures // Selected measures
Selected.Columns // Selected columns
Selected.Hierarchies // Selected hierarchies
// Singular form - single object (error if not exactly one selected)
Selected.Table // The single selected table
Selected.Measure // The single selected measure
Selected.Column // The single selected column
// Set properties on multiple objects at once
Selected.Measures.DisplayFolder = "Test";
Selected.Columns.IsHidden = true;
// Bulk rename with pattern
Selected.Measures.Rename("Amount", "Value");
When a Display Folder is selected, all child items are included in the selection.
LINQ is essential for filtering and transforming TOM collections. See references/linq-reference.md for the full method table, lambda syntax, and examples.
Key methods: Where(), First(), FirstOrDefault(), Any(), All(), Count(), Select(), OrderBy(), ForEach(), ToList().
// Common pattern: filter, chain, act
Model.AllMeasures
.Where(m => m.Name.Contains("Revenue"))
.Where(m => string.IsNullOrEmpty(m.FormatString))
.ForEach(m => m.FormatString = "$#,0");
See references/helper-methods.md for the complete reference including Output() variations, file operations, property export/import, interactive selection dialogs, DAX formatting/execution, and macro invocation.
| Method | Purpose |
|---|---|
Info(message) | Display info popup (CLI: writes to console) |
Warning(message) | Display warning popup |
Error(message) | Display error popup and stop script |
Output(object) | Display detailed object inspector dialog |
SaveFile(path, content) / ReadFile(path) | File I/O |
ExportProperties() / ImportProperties() | TSV export/import |
SelectMeasure() / SelectTable() | Interactive selection (IDE only) |
FormatDax() / CallDaxFormatter() | DAX formatting |
EvaluateDax() / ExecuteDax() | DAX execution (connected) |
For interactive dialogs (input forms, dropdowns, multi-select), see references/winforms-dialogs.md. Key setup: #r "System.Drawing", ScriptHelper.WaitFormVisible = false;
Add a Measure:
var m = Model.Tables["Sales"].AddMeasure("Total Revenue", "SUM(Sales[Amount])");
m.FormatString = "$#,0";
m.DisplayFolder = "Key Metrics";
m.Description = "Total sales revenue";
Info("Added: " + m.Name);
Iterate Tables/Columns:
foreach(var t in Model.Tables) {
foreach(var c in t.Columns.Where(c => c.Name.EndsWith("Key"))) {
c.IsHidden = true;
}
}
Info("Hidden key columns");
Conditional Operations:
foreach(var m in Model.AllMeasures) {
if(m.Name.Contains("Revenue")) m.FormatString = "$#,0";
if(m.Name.Contains("Rate")) m.FormatString = "0.00%";
}
Create Calculation Group:
var cg = Model.AddCalculationGroup("Time Intelligence");
cg.Precedence = 10;
var ytd = cg.AddCalculationItem("YTD", "CALCULATE(SELECTEDMEASURE(), DATESYTD('Date'[Date]))");
var prior = cg.AddCalculationItem("Prior Year");
prior.Expression = @"
CALCULATE(
SELECTEDMEASURE(),
DATEADD('Date'[Date], -1, YEAR)
)
";
Info("Created calculation group");
| Object | Access | Common Properties |
|---|---|---|
| Model | Model | .Tables, .AllMeasures, .Relationships |
| Table | Model.Tables["Name"] | .Measures, .Columns, .Partitions, .IsHidden |
| Measure | Table.Measures["Name"] | .Expression, .FormatString, .DisplayFolder, .Description |
| Column | Table.Columns["Name"] | .DataType, .FormatString, .IsHidden, .SummarizeBy |
| Relationship | Model.Relationships | .FromTable, .ToTable, .IsActive, .CrossFilteringBehavior |
| Role | Model.Roles["Name"] | .Members, .TablePermissions |
| Hierarchy | Table.Hierarchies | .Levels, .DisplayFolder, .IsHidden |
| Partition | Table.Partitions | .Expression, .SourceType, .DataSource |
| Perspective | Model.Perspectives | Objects have .InPerspective["Name"] |
| Culture | Model.Cultures | Objects have .TranslatedNames["culture"] |
Detailed documentation for each object type in object-types/:
tables.md - Table CRUD, properties, partitionscolumns.md - Column types, properties, sortingmeasures.md - Measure creation, formatting, organizationrelationships.md - Relationship managementcalculation-groups.md - Calculation groups and itemsroles.md - Roles, RLS, OLS configurationhierarchies.md - Hierarchy and level managementpartitions.md - Partition types and configurationperspectives.md - Perspective membershiptranslations.md - Culture and translation managementannotations.md - Custom metadata annotations180 working .csx scripts organized by category in examples/. Before writing a script from scratch, check if a relevant example already exists -- read the example, adapt it to the task, and modify as needed.
| Category | Scripts | Description |
|---|---|---|
bulk-operations/ | 8 | Model initialization, batch updates, clean names, sync folders, validate |
calculation-groups/ | 2 | Time intelligence, currency conversion |
columns/ | 20 | Data types, hiding, sorting, encoding hints, cardinality, properties |
cultures/ | 4 | Add/delete cultures, list, modify translations |
display-folders/ | 8 | Organize by type, clear, rename, add/remove folders |
evaluate-dax/ | 5 | Execute DAX, scalar queries, table queries, column sizes, optimize |
format-dax/ | 8 | Format measures, calculated columns/tables, KPIs, detail rows |
format-strings/ | 6 | Apply by name/pattern, custom formats, dynamic format strings |
functions/ | 4 | Add/delete/list/modify shared M functions |
hierarchies/ | 4 | Add/delete/list/modify hierarchies |
kpis/ | 4 | Add/delete/list/modify KPIs |
measures/ | 18 | Full CRUD, time intelligence, bulk create, move, hide/unhide |
model/ | 10 | Properties, compatibility level, dependencies, refresh, export stats |
partitions/ | 6 | Refresh, find-replace M, incremental refresh, hybrid tables |
perspectives/ | 4 | Add/delete/list/modify perspectives |
relationships/ | 5 | Create, naming conventions, RI violations, integrity, properties |
roles/ | 7 | Add/delete roles, members, RLS, OLS configuration |
shared-expressions/ | 6 | Named expressions, range parameters, M functions |
svg-measures/ | 15 | Bar charts, bullet charts, dumbbells, lollipops, waterfall, jitter |
tables/ | 36 | All table types (import, DirectQuery, Direct Lake, calculated, field parameter, date), refresh policies, RLS/OLS, properties |
See references/common-workflows.md for complete workflow scripts: bulk format measures, create time intelligence measures, configure RLS, audit hidden objects. Also check examples/ for 180 working .csx scripts before writing from scratch.
Add Info() checkpoints to find where script fails:
Info("Step 1: Starting");
var table = Model.Tables["Sales"];
Info("Step 2: Got table");
var measure = table.AddMeasure("Test", "1");
Info("Step 3: Added measure"); // If this doesn't appear, AddMeasure failed
Check existence before accessing:
if(Model.Tables.Contains("Sales")) {
var table = Model.Tables["Sales"];
// ...
} else {
Error("Table 'Sales' not found");
}
// Or use FirstOrDefault
var table = Model.Tables.FirstOrDefault(t => t.Name == "Sales");
if(table == null) {
Error("Table not found");
}
Info() after each operation)Use preprocessor directives for version-specific code:
#if TE3
// TE3-specific code (version 3.10.0+)
Info("Running in Tabular Editor 3");
#else
// TE2 fallback
Info("Running in Tabular Editor 2");
#endif
Check version at runtime:
var majorVersion = Selected.GetType().Assembly.GetName().Version.Major;
if(majorVersion >= 3) {
// TE3 code
}
.Contains() or .Any() before accessingScriptHelper.WaitFormVisible = false; for UI dialogsobject-types/ - Detailed API docs per object typeexamples/ - 180 working .csx scripts across 20 categories; always check here before writing from scratchTo retrieve current TOM API reference docs, use microsoft_docs_search + microsoft_docs_fetch (MCP) if available, otherwise mslearn search + mslearn fetch (CLI). Search based on the user's request and run multiple searches as needed to ensure sufficient context before proceeding.