
Programmatic Development
- 71 installs
- 50 repo stars
- Updated June 18, 2026
- josiahsiegel/claude-plugin-marketplace
Helps with ai & agent building tasks.
About
programmatic-development is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- programmatic-development
- AI & Agent Building
- AI-coding skill
Programmatic Development by the numbers
- 71 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #5,673 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill programmatic-developmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 71 |
|---|---|
| repo stars | ★ 50 |
| Last updated | June 18, 2026 |
| Repository | josiahsiegel/claude-plugin-marketplace ↗ |
What it does
Helps with ai & agent building tasks.
Files
Programmatic Power BI Development
Overview
Power BI supports multiple approaches for creating and managing reports and semantic models through code. As of 2026, the canonical stack is:
- PBIP (Power BI Project) as the folder-based source format
- TMDL as the semantic-model format inside PBIP (see
tmdl-masteryskill) - PBIR (Power BI Enhanced Report Format) as the report format inside PBIP
- fabric-cicd (Python) or Fabric CLI `fab deploy` for deployment
- TOM / .NET SDK for advanced programmatic model editing
- semantic-link-labs for Python-based scripting from Fabric notebooks
Reference Map
Detailed material lives in references/. Load only what the current task needs.
| Topic | File | When to load |
|---|---|---|
| PBIR rollout timeline, full PBIP folder structure, byPath/byConnection, annotations, validation, limitations | references/pbir-format-deep-dive.md | Authoring PBIR by hand, debugging definition.pbir, validating generated PBIR, understanding 2026 rollout state |
| PBIR JSON schema (visuals, pages, report settings) with Python manipulation examples | references/pbir-schema-reference.md | Scripting PBIR with Python: add page, batch-update visuals, copy between reports |
| TOM/Tabular Editor/fabric-cicd/Fabric CLI/semantic-link/pbi-tools/ALM Toolkit -- setup, examples, Desktop developer features | references/tools-and-tabular-editor.md | Choosing a tool, deploying, scripting TOM, running C# scripts, configuring Desktop |
| TOM advanced patterns (RLS, OLS, partitions, perspectives, translations, calc groups, SP auth) | references/tom-advanced-patterns.md | Writing complex TOM .NET code beyond basic measure/table edits |
| fabric-cicd advanced recipes (multi-workspace, hooks, GitHub Actions/Azure DevOps pipelines, parameter patterns, troubleshooting) | references/fabric-cicd-recipes.md | Production CI/CD workflows, multi-environment promotion, OIDC federated credentials |
PBIR - Power BI Enhanced Report Format
PBIR is Microsoft's modern, publicly documented, folder-based, JSON report format. It replaces the opaque report.json blob (now called PBIR-Legacy) with one file per visual, page, and bookmark, enabling proper Git diff/merge, code review, and schema-validated editing in VS Code.
Current state (April 2026): PBIR is the default for newly created reports in the Service. Existing Service reports are being auto-upgraded gradually. Desktop still needs the preview toggle until the May 2026 release, when PBIR becomes the Desktop default. PBIR-Legacy will be deprecated at PBIP GA.
For the full 2026 rollout timeline, admin opt-out, Service/Desktop restore policies, Sovereign Cloud caveats, complete project structure, byPath vs byConnection examples, annotations, self-validation procedure, and limitations, see references/pbir-format-deep-dive.md.
Quick PBIP structure (essentials only):
MyProject.pbip # Entry JSON
├── MyProject.Report/
│ ├── definition.pbir # Required entry; uses byPath or byConnection
│ └── definition/ # PBIR folder (one file per page/visual/bookmark)
└── MyProject.SemanticModel/
├── definition.pbism
└── definition/ # TMDL folderTMDL - Tabular Model Definition Language
TMDL is the human-readable, source-control-friendly format for semantic model definitions. GA since 2025. For comprehensive TMDL coverage including complete syntax reference, all object types, CI/CD patterns, and deployment workflows, load the dedicated powerbi-master:tmdl-mastery skill.
Quick example:
table Sales
measure 'Total Sales' = SUM(Sales[Amount])
formatString: $ #,##0.00
displayFolder: Revenue
partition 'Sales-Partition' = m
mode: import
source =
let
Source = Sql.Database("server", "db"),
Sales = Source{[Schema="dbo",Item="Sales"]}[Data]
in
Sales
column Amount
dataType: decimal
sourceColumn: Amount
formatString: $ #,##0.00Core Workflow
1. Pick the source format — PBIP (modern, TMDL + PBIR) for new work; legacy PBIX only for Report Server or pre-PBIP projects. 2. Edit programmatically — VS Code for PBIR/TMDL JSON; Tabular Editor for model authoring; semantic-link-labs for Fabric notebooks; TOM .NET for custom apps. 3. Validate locally — JSON schema, PBI-InspectorV2, lineage walker, BPA. See powerbi-master:validation-testing skill. 4. Parameterize — parameter.yml for environment-specific GUIDs/connection strings. 5. Deploy — fabric-cicd (Python library, primary) or fab deploy (CLI wrapper). Both consume the same parameter.yml. First deploy requires manual data-source credential entry in the Fabric portal. 6. Diff and promote — ALM Toolkit for schema diff; Fabric Deployment Pipelines or scripted multi-workspace deploy for promotion.
Deployment Decision Matrix (2026)
| Scenario | Recommended Tool |
|---|---|
| PBIP project with CI/CD (GitHub Actions / Azure DevOps) | fabric-cicd (Python) |
| Local ad-hoc deploy of a PBIP | fab deploy (Fabric CLI v1.5+) |
| Fabric notebook-based model editing | semantic-link-labs (sempy_labs) |
| Pure semantic model via XMLA | TOM (.NET) via Microsoft.AnalysisServices.NetCore.retail.amd64 |
| TMDL folder -> XMLA | Tabular Editor 2 CLI (-D switch) |
| Legacy PBIX only (no PBIP) | pbi-tools |
| Cross-environment promotion (dev/test/prod) | Fabric Deployment Pipelines (GUI) OR fabric-cicd with parameter.yml |
| Schema diff between two models | ALM Toolkit |
Tool Quick-Pick
| Need | Load reference | Tool |
|---|---|---|
| Modify a measure on a deployed model | tools-and-tabular-editor.md | TOM or semantic-link-labs |
| Auto-generate measures across folders | tools-and-tabular-editor.md | Tabular Editor C# script |
| Deploy PBIP from CI | fabric-cicd-recipes.md | fabric-cicd + parameter.yml |
| Generate PBIR pages from a template | pbir-schema-reference.md | Python + JSON schema validation |
| RLS/OLS in .NET | tom-advanced-patterns.md | TOM |
| Extract legacy PBIX for diff | tools-and-tabular-editor.md | pbi-tools |
Related Skills
powerbi-master:tmdl-mastery-- Deep TMDL language reference and syntaxpowerbi-master:validation-testing-- Validate generated PBIR / TMDL / DAX before deploypowerbi-master:rest-api-automation-- Raw Fabric REST API endpoints when fabric-cicd doesn't cover a scenariopowerbi-master:deployment-admin-- Fabric Deployment Pipelines, workspace management, RLS at deploy timepowerbi-master:fabric-integration-- Direct Lake, OneLake, Lakehouse/Warehouse integration
Official Microsoft References (2026)
fabric-cicd Advanced Recipes (2026)
Production-grade patterns for deploying Power BI Projects (PBIP) and other Fabric items using fabric-cicd -- Microsoft's officially supported Python library for Fabric deployment. This reference complements the SKILL.md quick start and covers multi-environment deployment, dependency ordering, hooks, and troubleshooting.
Library Overview
pip install fabric-cicd- Python support: 3.9 - 3.13
- Supported item types (24): ApacheAirflowJob, CopyJob, DataAgent, DataPipeline, Dataflow, Environment, Eventhouse, Eventstream, GraphQLApi, KQLDashboard, KQLDatabase, KQLQueryset, Lakehouse, MirroredDatabase, MLExperiment, MountedDataFactory, Notebook, Reflex, Report, SemanticModel, SparkJobDefinition, SQLDatabase, UserDataFunction, VariableLibrary, Warehouse
- Base deployment model: full deployment every run (no commit diff calculation)
- Dependencies handled automatically: SemanticModel deploys before Report; Lakehouse before Notebook that references it
- Tenant-scoped: deploys into the tenant of the executing identity
Core API
from fabric_cicd import (
FabricWorkspace,
publish_all_items,
unpublish_all_orphan_items,
append_feature_flag, # experimental feature toggles
)FabricWorkspace constructor
| Parameter | Type | Description |
|---|---|---|
workspace_name or workspace_id | str | Target workspace -- name or GUID (one required) |
repository_directory | str | Path to the PBIP/source folder |
item_type_in_scope | list[str] | Item types to deploy (e.g., ["SemanticModel", "Report"]) |
environment | str | Environment key used to resolve parameter.yml (e.g., "dev", "prod") |
token_credential | TokenCredential | Azure Identity credential (optional -- defaults to DefaultAzureCredential) |
base_api_url | str | Override Fabric API base URL (for Sovereign Clouds) |
Publish and cleanup
publish_all_items(target_workspace)
unpublish_all_orphan_items(target_workspace, item_name_exclude_regex=r".*_keep$")unpublish_all_orphan_items removes workspace items that no longer exist in the repo. Use item_name_exclude_regex to protect items that should never be auto-deleted.
Authentication Patterns
Interactive Browser (local dev)
from azure.identity import InteractiveBrowserCredential
credential = InteractiveBrowserCredential()Azure CLI (local dev after az login)
from azure.identity import AzureCliCredential
credential = AzureCliCredential()Service Principal (CI/CD)
import os
from azure.identity import ClientSecretCredential
credential = ClientSecretCredential(
tenant_id=os.environ["AZURE_TENANT_ID"],
client_id=os.environ["AZURE_CLIENT_ID"],
client_secret=os.environ["AZURE_CLIENT_SECRET"],
)GitHub OIDC Federated Credentials (CI/CD, keyless)
# No client secret needed -- GitHub Actions provides an OIDC token
from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()In the GitHub Actions workflow, configure azure/login@v2 with a service principal that has a federated identity credential linked to the repo. This avoids storing secrets entirely.
Managed Identity (Azure-hosted runners)
from azure.identity import ManagedIdentityCredential
credential = ManagedIdentityCredential()parameter.yml Deep Dive
The parameter.yml file lives in repository_directory and supports two replacement modes:
1. find_replace (text substitution)
Substitutes literal strings anywhere in PBIP definition files:
find_replace:
- find_value: "sql-dev.database.windows.net"
replace_value:
dev: "sql-dev.database.windows.net"
test: "sql-test.database.windows.net"
prod: "sql-prod.database.windows.net"
item_type:
- SemanticModel
item_name:
- SalesModel
file_path:
- "SalesModel.SemanticModel/definition/expressions.tmdl"Optional filters narrow the scope of replacement:
item_type-- only in specified item typesitem_name-- only in specified items by namefile_path-- only in specified files (glob supported)
2. key_value_replace (structured JSON/YAML)
For structured replacements like Direct Lake connection GUIDs:
key_value_replace:
- find_key: "$.datasetReference.byConnection.connectionString"
replace_value:
dev: "semanticmodelid=dev-guid"
prod: "semanticmodelid=prod-guid"
item_type:
- Report
file_path:
- "*.Report/definition.pbir"find_key is a JSONPath expression.
Built-in fabric-cicd tokens
Some special tokens get auto-resolved by the library:
find_replace:
- find_value: "$workspace_id"
replace_value:
dev: "{{TARGET_WORKSPACE_ID}}" # replaced with actual target workspace GUID
prod: "{{TARGET_WORKSPACE_ID}}"Multi-Workspace Deployment
Deploy the same source to multiple workspaces (e.g., per business unit) in one run:
from fabric_cicd import FabricWorkspace, publish_all_items
workspaces = [
("BU1-Sales-Dev", "bu1_dev"),
("BU2-Sales-Dev", "bu2_dev"),
("BU3-Sales-Dev", "bu3_dev"),
]
for ws_name, env_name in workspaces:
print(f"Deploying to {ws_name}...")
target = FabricWorkspace(
workspace_name=ws_name,
environment=env_name,
repository_directory=".",
item_type_in_scope=["SemanticModel", "Report"],
token_credential=credential,
)
publish_all_items(target)Keep each environment's overrides in the same parameter.yml under different environment keys.
Selective Deployment by Item Type
Deploy only semantic models (skip reports):
target = FabricWorkspace(
workspace_name="Sales-Dev",
environment="dev",
repository_directory=".",
item_type_in_scope=["SemanticModel"], # Reports not deployed
token_credential=credential,
)
publish_all_items(target)Deploy only specific items by name:
from fabric_cicd import publish_all_items
publish_all_items(
target,
items_to_include=["SalesModel", "SalesOverview"], # Name filter
)Experimental Feature Flags
Enable preview features via append_feature_flag:
from fabric_cicd import append_feature_flag
append_feature_flag("enable_shortcut_publish") # Lakehouse shortcuts
append_feature_flag("disable_print_diff") # Suppress diff output
append_feature_flag("enable_experimental_items") # Allow items not yet in supported listPre/Post Hooks via Wrapper Functions
fabric-cicd does not expose native hooks, but you can wrap publish_all_items to run arbitrary Python before and after:
def run_bpa_before_deploy(workspace, dataset):
import sempy_labs as labs
results = labs.run_model_bpa(dataset=dataset, workspace=workspace)
errors = results[results["Severity"] == "Error"]
if len(errors) > 0:
display(errors)
raise RuntimeError(f"BPA blocked: {len(errors)} error-severity rules")
def refresh_after_deploy(workspace, dataset):
import sempy.fabric as fabric
fabric.refresh_dataset(dataset=dataset, workspace=workspace, refresh_type="Full")
# Wrap
run_bpa_before_deploy("Sales-Dev", "SalesModel")
publish_all_items(target_workspace)
refresh_after_deploy("Sales-Prod", "SalesModel")Dependency Ordering
fabric-cicd automatically orders deployments based on known dependencies:
1. Lakehouse -> Notebooks/SemanticModels that reference it 2. Warehouse -> Dataflows/SemanticModels 3. SemanticModel -> Reports 4. Environment -> Notebooks/Spark jobs that use it 5. VariableLibrary -> Items that reference its variables
For custom ordering (e.g., Dataflow Gen2 that must exist before a semantic model), split into two publish_all_items calls:
# Stage 1: Foundation
stage1 = FabricWorkspace(..., item_type_in_scope=["Lakehouse", "Dataflow", "Warehouse"])
publish_all_items(stage1)
# Stage 2: Semantic models that depend on foundation
stage2 = FabricWorkspace(..., item_type_in_scope=["SemanticModel"])
publish_all_items(stage2)
# Stage 3: Reports that depend on semantic models
stage3 = FabricWorkspace(..., item_type_in_scope=["Report"])
publish_all_items(stage3)GitHub Actions with Federated Credentials (Keyless)
Recommended production pattern -- no secrets stored in GitHub:
name: Deploy PBIP (OIDC)
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: '3.12' }
- name: Azure Login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
allow-no-subscriptions: true
- name: Install fabric-cicd
run: pip install fabric-cicd
- name: Deploy
run: |
python - <<'PY'
from azure.identity import AzureCliCredential
from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items
target = FabricWorkspace(
workspace_name="Sales-Prod",
environment="prod",
repository_directory=".",
item_type_in_scope=["SemanticModel", "Report"],
token_credential=AzureCliCredential(),
)
publish_all_items(target)
unpublish_all_orphan_items(target)
PYConfigure federated credentials on the service principal in Azure AD:
- Issuer:
https://token.actions.githubusercontent.com - Subject:
repo:org/repo:ref:refs/heads/main(orenvironment:productionfor environment-scoped) - Audience:
api://AzureADTokenExchange
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
401 Unauthorized on deploy | Service principal missing workspace role | Add SP as Contributor/Admin to workspace |
403 Forbidden: service principals not enabled | Tenant setting disabled | Enable "Service principals can call Fabric public APIs" in Admin portal |
| Semantic model deploys but report fails with "dataset not found" | byConnection reference uses wrong semantic model GUID | Either let fabric-cicd resolve byPath, or update parameter.yml to replace the GUID |
| Deployment hangs on large semantic model | Item import timeout | Split model into smaller sub-models or use Tabular Editor CLI with XMLA endpoint directly |
parameter.yml substitution not applied | Environment key typo | Ensure environment parameter matches a key under replace_value exactly |
| Orphan cleanup deletes items unexpectedly | Missing items in repo | Use item_name_exclude_regex to protect; or avoid unpublish_all_orphan_items entirely |
| Direct Lake model loses connection after deploy | Source lakehouse/warehouse GUID not parameterized | Add find_replace or key_value_replace entries for lakehouse GUIDs |
ModuleNotFoundError: No module named 'fabric_cicd' | Wrong Python environment | Pin Python 3.12 in workflow; use pip install --upgrade fabric-cicd |
| Data source credentials prompt after every deploy | First-deploy behavior | Set credentials once in Fabric portal; subsequent deploys reuse them |
403 FabricItemNotAuthorized on Direct Lake | SP missing Viewer role on Lakehouse | Grant SP at least Viewer on the source Lakehouse/Warehouse |
CannotOverwriteModifiedItem error | Item edited in Service since last deploy | Either force overwrite (via feature flag), or commit Service changes back to Git first |
Comparison: fabric-cicd vs Fabric Deployment Pipelines vs Manual REST
| Feature | fabric-cicd | Fabric Deployment Pipelines (GUI) | Raw REST API |
|---|---|---|---|
| Source of truth | Git repo | Dev workspace | Your own |
| Parameterization | parameter.yml | Rules engine (GUI) | Custom |
| Multi-item dependencies | Auto-resolved | Auto-resolved | Manual |
| Audit trail | Git history | Fabric portal | Custom |
| Authentication | Azure Identity | User/SP | User/SP |
| Orphan cleanup | Built-in | Manual | Custom |
| Python/CI friendly | Yes | Limited (REST API only) | Yes |
| Zero-code option | No | Yes | No |
| Best for | Code-first teams | Citizen developers | Custom tooling |
References
PBIR Format Deep Dive
Reference for the Power BI Enhanced Report Format: rollout timeline, full project structure, byPath/byConnection references, annotations, validation, and limitations.
2026 Rollout Timeline (as of April 2026)
| Milestone | Date | Status |
|---|---|---|
| PBIR public preview in Desktop | 2024 | Preview |
| PBIR default in Power BI Service (new reports) | January 25, 2026 | Rolling out |
| PBIR automatic upgrade of existing Service reports | January -- end of April 2026 | Rolling out (gradual, by report size -- reports with <100 visuals first) |
| PBIR default in Power BI Desktop | May 2026 release | Planned (delayed from March 2026) |
| PBIP (PBIR + TMDL) GA | 2026 | Planned |
| PBIR-Legacy deprecation | At PBIR GA | Planned ("PBIR-Legacy will no longer be supported") |
Current state (April 2026): PBIR is the default for all newly created reports in the Power BI Service. Existing Service reports are being auto-upgraded as they are edited. Power BI Desktop still requires the preview feature toggle, but that changes in the May 2026 release. PBIR-Legacy remains supported during the transition.
Admin opt-out: Tenants can temporarily opt out via the tenant setting "Automatically convert and store reports in the Power BI enhanced metadata format (PBIR)", but this opt-out will be removed at GA.
Service restore: When an existing report is auto-upgraded in the Service, a PBIR-Legacy backup is retained for 28 days. Restore via Report Settings > "Restore as PBIR-Legacy". Desktop upgrades keep a 30-day backup in %USERPROFILE%\AppData\Local\Microsoft\Power BI Desktop\TempSaves\Backups (or the Store app equivalent).
Sovereign Clouds: PBIR will NOT be automatically upgraded in Sovereign Clouds prior to GA. Sovereign Cloud customers can still test PBIR via the Desktop preview feature.
PBIR on Report Server: Not supported. Report Server continues to use the legacy PBIX binary format only.
PBIP Project Structure (2026)
A PBIP project is a folder containing a .pbip entry file, one *.Report/ folder, and one *.SemanticModel/ folder (historically called *.Dataset/). The modern naming is SemanticModel; Desktop writes the new name by default.
MyProject.pbip # Entry file (JSON, double-click to open)
├── MyProject.Report/
│ ├── definition.pbir # Required -- report definition entry
│ ├── definition/ # PBIR folder (replaces legacy report.json)
│ │ ├── report.json # Report-level settings, theme, filters
│ │ ├── version.json # PBIR schema version
│ │ ├── reportExtensions.json # Optional -- report-level measures
│ │ ├── pages/
│ │ │ ├── pages.json # Page order and active page
│ │ │ ├── <pageName>/
│ │ │ │ ├── page.json # Page metadata, filters
│ │ │ │ └── visuals/
│ │ │ │ └── <visualName>/
│ │ │ │ ├── visual.json # Visual definition (query, formatting)
│ │ │ │ └── mobile.json # Optional -- mobile layout override
│ │ └── bookmarks/
│ │ ├── bookmarks.json # Bookmark order and groups
│ │ └── <bookmarkName>.bookmark.json
│ ├── CustomVisuals/ # Private .pbiviz packages
│ ├── StaticResources/
│ │ └── RegisteredResources/ # Custom themes, images
│ ├── semanticModelDiagramLayout.json
│ ├── mobileState.json # Report-level mobile state (not editable externally)
│ ├── .pbi/
│ │ └── localSettings.json # User-specific, gitignored
│ └── .platform # Fabric Git integration system file
├── MyProject.SemanticModel/
│ ├── definition.pbism # Required -- semantic model entry
│ ├── definition/ # TMDL folder (replaces model.bim)
│ │ ├── database.tmdl
│ │ ├── model.tmdl
│ │ ├── relationships.tmdl
│ │ ├── expressions.tmdl
│ │ ├── tables/
│ │ │ └── *.tmdl
│ │ ├── roles/
│ │ ├── cultures/
│ │ └── perspectives/
│ ├── diagramLayout.json
│ └── .pbi/
│ ├── localSettings.json # Gitignored
│ └── cache.abf # Gitignored (local data cache)
└── .gitignoreKey points:
definition.pbir(singular, at report root) is required.report.jsonat the root is the legacy PBIR-Legacy file;definition/report.jsonis the new PBIR report-level settings file.version.jsoninsidedefinition/declares the PBIR schema version.- By default, PBIR folder names for pages/visuals/bookmarks are 20-character GUIDs like
90c2e07d8e84e7d5c026. They can be renamed, but the objectnameproperty inside the JSON must remain unique; restart Desktop after renaming. - For Fabric REST API deployment,
definition.pbirmust use abyConnectionreference (notbyPath) with aconnectionStringcontainingsemanticmodelid=<guid>. - PBIR supports up to 1,000 pages per report, 1,000 visuals per page, 300 MB per report (service-enforced).
Definition.pbir -- byPath vs byConnection
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definitionProperties/2.0.0/schema.json",
"version": "4.0",
"datasetReference": {
"byPath": { "path": "../MyProject.SemanticModel" }
}
}For remote (live-connect) semantic models, use byConnection. When deploying via the Fabric REST API, only the semanticmodelid is required:
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definitionProperties/2.0.0/schema.json",
"version": "4.0",
"datasetReference": {
"byConnection": {
"connectionString": "semanticmodelid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
}You can have *multiple `.pbir files** in the same Report folder (e.g., definition.pbir + definition-liveConnect.pbir). Fabric Git integration only processes definition.pbir`; the others are preserved but ignored.
PBIR Report JSON Schema
Each visual is a separate JSON file with a schema declaration:
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/1.0.0/schema.json",
"name": "uniqueVisualId",
"position": {
"x": 50, "y": 50,
"width": 400, "height": 300,
"tabOrder": 0
},
"visual": {
"visualType": "barChart",
"query": {
"queryState": {
"Category": {
"projections": [{ "queryRef": "Product.Category", "active": true }]
},
"Y": {
"projections": [{ "queryRef": "Sum(Sales.Amount)" }]
}
}
},
"objects": {
"legend": [{ "properties": { "show": { "expr": { "Literal": { "Value": "true" } } } } }]
}
}
}Programmatic PBIR Manipulation
Since PBIR files are schema-validated JSON, you can create or modify reports with any language. Every file includes a $schema URL pointing to the public JSON schema, so VS Code, PyCharm, and other editors provide full IntelliSense and validation while editing.
Common scenarios enabled by the file-per-object layout:
- Copy pages, visuals, or bookmarks between reports (file copy, no Desktop required)
- Batch-update a property across every visual (e.g., hide filter pane on all visuals)
- Script-generate entire pages from a data-driven template
- Find-and-replace field references across an entire report for a rename refactor
For complete Python examples (add page, batch update all visuals, copy visual between reports), see pbir-schema-reference.md.
PBIR Annotations (Custom Deployment Metadata)
You can embed name-value annotations inside report.json, page.json, or visual.json. Power BI Desktop ignores them, but deployment scripts can read them as configuration:
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/report/1.0.0/schema.json",
"themeCollection": {
"baseTheme": {"name": "CY24SU06", "type": "SharedResources"}
},
"annotations": [
{"name": "defaultPage", "value": "c2d9b4b1487b2eb30e98"},
{"name": "deploymentTier", "value": "production"},
{"name": "owner", "value": "analytics-team@contoso.com"}
]
}Self-Validation of Generated PBIR
Before committing or deploying any PBIR you've generated, validate it locally. Every PBIR file embeds a $schema URL pointing to the official Microsoft schema in microsoft/json-schemas, which means standard JSON Schema validators (Python jsonschema, VS Code) catch syntax errors offline.
Three layers to run on every PBIR change:
1. JSON schema -- python -m jsonschema against each *.json file using the embedded $schema URL 2. Rules -- PBI-InspectorV2 (Fab Inspector) v2.3+ runs the rules-based PBIR/PBIP validator with the -fabricitem switch and supports the new enhanced PBIR format (the original PBI-Inspector repo only handles PBIR-Legacy) 3. Lineage -- a custom Python walker that verifies bookmarks reference real pages, drillthrough targets exist, and theme files are present
For full recipes including a GitHub Actions CI gate template, see the powerbi-master:validation-testing skill.
PBIR Limitations to Know
- Large reports (>500 files) can experience authoring performance issues in Desktop (viewing is not affected).
- Filter pane must be expanded at least once for automatic visual filters to persist to
visual.json. - Bookmarks capture visual state from the original page; copying a bookmark to a report without the source visuals drops invalid visual state.
- pageBinding.name must be unique across the report (used for drillthrough/tooltip pages). After June 2024, new
pageBindingnames are GUIDs by default to avoid collisions. - Renaming folders requires a Desktop restart and preserves the original name on save unless you also update the
nameproperty inside the JSON. - Not supported in Template App workspaces.
PBIR JSON Schema Reference
Report Definition (definition.pbir)
{
"version": "1.0",
"datasetReference": {
"byPath": {
"path": "../MyReport.dataset"
},
"byConnection": null
}
}When connecting to a remote (published) semantic model:
{
"version": "1.0",
"datasetReference": {
"byPath": null,
"byConnection": {
"connectionString": "Data Source=powerbi://api.powerbi.com/v1.0/myorg/WorkspaceName;Initial Catalog=SemanticModelName",
"pbiServiceModelId": null,
"pbiModelVirtualServerName": "sobe_wowvirtualserver",
"pbiModelDatabaseName": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"name": "EntityDataSource",
"connectionType": "pbiServiceXmlaStyleLive"
}
}
}Page Definition (page.json)
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/page/1.0.0/schema.json",
"name": "ReportSection1",
"displayName": "Sales Overview",
"displayOption": 0,
"height": 720,
"width": 1280,
"filters": [],
"ordinal": 0,
"visibility": 0
}Display options:
| Value | Mode |
|---|---|
| 0 | Fit to page (default) |
| 1 | Fit to width |
| 2 | Actual size |
Visibility:
| Value | State |
|---|---|
| 0 | Visible |
| 1 | Hidden |
Visual Definition (visual.json)
Basic Structure
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/1.0.0/schema.json",
"name": "abc123def456",
"position": {
"x": 50,
"y": 50,
"z": 0,
"width": 400,
"height": 300,
"tabOrder": 1000
},
"visual": {
"visualType": "clusteredBarChart",
"query": { ... },
"objects": { ... },
"drillFilterOtherVisuals": true
},
"filters": [ ... ]
}Common Visual Types
| visualType | Description |
|---|---|
barChart | Stacked bar chart |
clusteredBarChart | Clustered bar chart |
columnChart | Stacked column chart |
clusteredColumnChart | Clustered column chart |
lineChart | Line chart |
areaChart | Area chart |
lineStackedColumnComboChart | Line and column combo |
lineClusteredColumnComboChart | Line and clustered column combo |
pieChart | Pie chart |
donutChart | Donut chart |
treemap | Treemap |
waterfallChart | Waterfall chart |
funnel | Funnel chart |
card | Single-value card |
multiRowCard | Multi-row card |
kpi | KPI visual |
slicer | Slicer |
tableEx | Table |
pivotTable | Matrix |
map | Map (Bing) |
filledMap | Filled map (choropleth) |
shapeMap | Shape map |
azureMap | Azure Map |
gauge | Gauge |
scatterChart | Scatter plot |
ribbonChart | Ribbon chart |
decompositionTreeVisual | Decomposition tree |
keyInfluencers | Key influencers |
qnaVisual | Q&A visual |
smartNarrativeVisual | Smart narrative |
actionButton | Button |
bookmarkNavigator | Bookmark navigator |
pageNavigator | Page navigator |
textbox | Text box |
image | Image |
shape | Shape |
Query State Structure
{
"queryState": {
"Category": {
"projections": [
{
"queryRef": "Product.Category",
"active": true
}
]
},
"Y": {
"projections": [
{
"queryRef": "Sum(Sales.Amount)",
"active": true
}
]
},
"Series": {
"projections": [
{
"queryRef": "Date.Year",
"active": true
}
]
}
}
}Query buckets by visual type:
| Visual | Buckets |
|---|---|
| Bar/Column chart | Category, Y, Series, Tooltips |
| Line chart | Category, Y, Series, Tooltips |
| Pie/Donut | Category, Y, Tooltips |
| Table | Values |
| Matrix | Rows, Columns, Values |
| Card | Fields |
| Slicer | Fields |
| Map | Location, Legend, Size, Tooltips |
| Scatter | X, Y, Size, Details, Legend |
Visual Objects (Formatting)
{
"objects": {
"title": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"text": { "expr": { "Literal": { "Value": "'Sales by Category'" } } },
"fontSize": { "expr": { "Literal": { "Value": "14D" } } },
"fontColor": { "solid": { "color": { "expr": { "Literal": { "Value": "'#333333'" } } } } }
}
}],
"legend": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"position": { "expr": { "Literal": { "Value": "'Right'" } } }
}
}],
"dataLabels": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "false" } } }
}
}],
"categoryAxis": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } }
}
}],
"valueAxis": [{
"properties": {
"show": { "expr": { "Literal": { "Value": "true" } } },
"start": { "expr": { "Literal": { "Value": "0D" } } }
}
}]
}
}Filter Definitions
Visual-level filter:
{
"filters": [
{
"name": "Filter_abc123",
"expression": {
"Column": {
"Expression": { "SourceRef": { "Entity": "Sales" } },
"Property": "Status"
}
},
"type": "Categorical",
"filter": {
"Version": 2,
"From": [{ "Name": "s", "Entity": "Sales", "Type": 0 }],
"Where": [{
"Condition": {
"In": {
"Expressions": [{ "Column": { "Expression": { "SourceRef": { "Source": "s" } }, "Property": "Status" } }],
"Values": [[{ "Literal": { "Value": "'Active'" } }]]
}
}
}]
}
}
]
}Report Settings (report.json)
{
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/report/1.0.0/schema.json",
"themeCollection": {
"baseTheme": {
"name": "CY24SU06",
"reportVersionAtImport": "5.53",
"type": 2
}
},
"activeSectionIndex": 0,
"settings": {
"filterPaneEnabled": true,
"navContentPaneEnabled": true,
"useStylableVisualContainerHeader": true,
"exportDataMode": 1,
"queryLimitOption": 3
},
"resourcePackages": []
}Programmatic Report Generation Template (Python)
Complete template for generating a multi-page PBIR report:
import json
import os
import uuid
def create_report(report_name, dataset_path, pages):
"""Generate a complete PBIR report structure."""
base_dir = f"{report_name}.report"
os.makedirs(base_dir, exist_ok=True)
# definition.pbir
with open(f"{base_dir}/definition.pbir", "w") as f:
json.dump({
"version": "1.0",
"datasetReference": {
"byPath": {"path": f"../{dataset_path}"},
"byConnection": None
}
}, f, indent=2)
# report.json
with open(f"{base_dir}/report.json", "w") as f:
json.dump({
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/report/1.0.0/schema.json",
"themeCollection": {"baseTheme": {"name": "CY24SU06", "type": 2}},
"activeSectionIndex": 0,
"settings": {"filterPaneEnabled": True}
}, f, indent=2)
# Pages
for i, page in enumerate(pages):
page_id = page.get("id", f"ReportSection{i}")
page_dir = f"{base_dir}/pages/{page_id}"
os.makedirs(f"{page_dir}/visuals", exist_ok=True)
with open(f"{page_dir}/page.json", "w") as f:
json.dump({
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/page/1.0.0/schema.json",
"name": page_id,
"displayName": page["name"],
"displayOption": 0,
"height": 720,
"width": 1280,
"ordinal": i
}, f, indent=2)
for visual in page.get("visuals", []):
vid = visual.get("id", uuid.uuid4().hex[:12])
visual_dir = f"{page_dir}/visuals/{vid}"
os.makedirs(visual_dir, exist_ok=True)
with open(f"{visual_dir}/visual.json", "w") as f:
json.dump(visual["definition"], f, indent=2)
# Usage
create_report("SalesReport", "SalesReport.dataset", [
{
"name": "Overview",
"visuals": [
{
"definition": {
"$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/1.0.0/schema.json",
"name": "card_revenue",
"position": {"x": 50, "y": 50, "width": 200, "height": 100},
"visual": {
"visualType": "card",
"query": {"queryState": {"Fields": {"projections": [{"queryRef": "Sum(Sales.Revenue)"}]}}}
}
}
}
]
}
])TOM Advanced Patterns
Creating Relationships
// One-to-many relationship
var relationship = new SingleColumnRelationship() {
Name = "Sales_to_Product",
FromTable = model.Tables["Sales"],
FromColumn = model.Tables["Sales"].Columns["ProductID"],
ToTable = model.Tables["Products"],
ToColumn = model.Tables["Products"].Columns["ProductID"],
FromCardinality = RelationshipEndCardinality.Many,
ToCardinality = RelationshipEndCardinality.One,
CrossFilteringBehavior = CrossFilteringBehavior.OneDirection,
IsActive = true,
SecurityFilteringBehavior = SecurityFilteringBehavior.OneDirection
};
model.Relationships.Add(relationship);Row-Level Security (RLS)
// Create a role with table filter
var role = new ModelRole() { Name = "RegionManager" };
var tablePermission = new TablePermission() {
Table = model.Tables["Sales"],
FilterExpression = "[Region] = USERPRINCIPALNAME()"
};
role.TablePermissions.Add(tablePermission);
model.Roles.Add(role);
// Add a member to the role (requires admin SDK/API, not TOM directly)
// Use REST API: POST /groups/{groupId}/datasets/{datasetId}/usersObject-Level Security (OLS)
// Hide columns from specific roles
var role = model.Roles["RestrictedUser"];
var columnPermission = new ColumnPermission() {
Column = model.Tables["Employees"].Columns["Salary"],
MetadataPermission = MetadataPermission.None // Column is invisible
};
var tablePermission = role.TablePermissions["Employees"];
if (tablePermission == null) {
tablePermission = new TablePermission() { Table = model.Tables["Employees"] };
role.TablePermissions.Add(tablePermission);
}
tablePermission.ColumnPermissions.Add(columnPermission);Partitions and Incremental Refresh
// Create partitioned table for incremental refresh
var table = model.Tables["Sales"];
// Remove default partition
table.Partitions.Clear();
// Add historical partition (import)
var historicalPartition = new Partition() {
Name = "Sales_Historical",
Mode = ModeType.Import,
Source = new MPartitionSource() {
Expression = @"
let
Source = Sql.Database(""server"", ""db""),
Sales = Source{[Schema=""dbo"",Item=""Sales""]}[Data],
Filtered = Table.SelectRows(Sales, each [OrderDate] < #date(2025, 1, 1))
in Filtered"
}
};
table.Partitions.Add(historicalPartition);
// Add current partition (could be DirectQuery for real-time)
var currentPartition = new Partition() {
Name = "Sales_Current",
Mode = ModeType.Import,
Source = new MPartitionSource() {
Expression = @"
let
Source = Sql.Database(""server"", ""db""),
Sales = Source{[Schema=""dbo"",Item=""Sales""]}[Data],
Filtered = Table.SelectRows(Sales, each [OrderDate] >= #date(2025, 1, 1))
in Filtered"
}
};
table.Partitions.Add(currentPartition);Perspectives
// Create a perspective (a view/subset of the model)
var perspective = new Perspective() { Name = "Sales Analysis" };
model.Perspectives.Add(perspective);
// Add tables/columns to perspective
var salesPerspective = new PerspectiveTable() { Table = model.Tables["Sales"] };
perspective.PerspectiveTables.Add(salesPerspective);
// Add specific columns (if you want to exclude some)
salesPerspective.PerspectiveColumns.Add(
new PerspectiveColumn() { Column = model.Tables["Sales"].Columns["Amount"] }
);
// Add measures
salesPerspective.PerspectiveMeasures.Add(
new PerspectiveMeasure() { Measure = model.Tables["Sales"].Measures["Total Sales"] }
);Translations (Localization)
// Add a culture/locale
var culture = new Culture() { Name = "es-ES" };
model.Cultures.Add(culture);
// Add translations for table
var tableTranslation = new ObjectTranslation() {
Object = model.Tables["Sales"],
Property = TranslatedProperty.Caption,
Value = "Ventas"
};
culture.ObjectTranslations.Add(tableTranslation);
// Add translations for column
var columnTranslation = new ObjectTranslation() {
Object = model.Tables["Sales"].Columns["Amount"],
Property = TranslatedProperty.Caption,
Value = "Cantidad"
};
culture.ObjectTranslations.Add(columnTranslation);
// Add translations for measure
var measureTranslation = new ObjectTranslation() {
Object = model.Tables["Sales"].Measures["Total Sales"],
Property = TranslatedProperty.Caption,
Value = "Ventas Totales"
};
culture.ObjectTranslations.Add(measureTranslation);Calculation Groups (TOM)
// Create calculation group table
var calcGroupTable = new Table() {
Name = "Time Intelligence",
CalculationGroup = new CalculationGroup()
};
// Add the Name column (required)
calcGroupTable.Columns.Add(new DataColumn() {
Name = "Time Calculation",
DataType = DataType.String,
SourceColumn = "Name",
SortByColumn = model.Tables["Time Intelligence"].Columns.ContainsName("Ordinal")
? model.Tables["Time Intelligence"].Columns["Ordinal"] : null
});
// Ordinal column for sort order
calcGroupTable.Columns.Add(new DataColumn() {
Name = "Ordinal",
DataType = DataType.Int64,
SourceColumn = "Ordinal",
IsHidden = true
});
// Add calculation items
calcGroupTable.CalculationGroup.CalculationItems.Add(new CalculationItem() {
Name = "Current",
Expression = "SELECTEDMEASURE()",
Ordinal = 0
});
calcGroupTable.CalculationGroup.CalculationItems.Add(new CalculationItem() {
Name = "YTD",
Expression = "CALCULATE(SELECTEDMEASURE(), DATESYTD('Date'[Date]))",
Ordinal = 1
});
calcGroupTable.CalculationGroup.CalculationItems.Add(new CalculationItem() {
Name = "PY",
Expression = "CALCULATE(SELECTEDMEASURE(), SAMEPERIODLASTYEAR('Date'[Date]))",
Ordinal = 2
});
calcGroupTable.CalculationGroup.CalculationItems.Add(new CalculationItem() {
Name = "YoY %",
Expression = @"
VAR Current = SELECTEDMEASURE()
VAR PY = CALCULATE(SELECTEDMEASURE(), SAMEPERIODLASTYEAR('Date'[Date]))
RETURN DIVIDE(Current - PY, PY)",
FormatStringDefinition = new FormatStringDefinition() {
Expression = """0.00%"""
},
Ordinal = 3
});
model.Tables.Add(calcGroupTable);Data Source with Service Principal Authentication
var dataSource = new StructuredDataSource() {
Name = "AzureSQL_ServicePrincipal",
ConnectionDetails = new ConnectionDetails() {
Protocol = "tds",
Address = new ConnectionAddress() {
Server = "server.database.windows.net",
Database = "mydb"
},
Authentication = null, // Authentication handled at runtime
Query = null
},
Credential = new Credential() {
AuthenticationKind = "ServicePrincipal",
// Actual credentials managed in Power BI Service
}
};
model.DataSources.Add(dataSource);Connecting to XMLA Endpoint
Service Principal Authentication
string clientId = "your-app-client-id";
string clientSecret = "your-client-secret";
string tenantId = "your-tenant-id";
string workspaceName = "Your Workspace";
string connectionString =
$"DataSource=powerbi://api.powerbi.com/v1.0/myorg/{workspaceName};" +
$"User ID=app:{clientId}@{tenantId};" +
$"Password={clientSecret};";
using var server = new Server();
server.Connect(connectionString);Azure AD Token Authentication
using Azure.Identity;
using Microsoft.AnalysisServices.Tabular;
var credential = new DefaultAzureCredential();
var token = await credential.GetTokenAsync(
new Azure.Core.TokenRequestContext(
new[] { "https://analysis.windows.net/powerbi/api/.default" }
)
);
string connectionString =
$"DataSource=powerbi://api.powerbi.com/v1.0/myorg/{workspaceName};" +
$"Password={token.Token};";
using var server = new Server();
server.Connect(connectionString);Refresh Operations via TOM
// Full refresh of a table
model.Tables["Sales"].RequestRefresh(RefreshType.Full);
// Incremental refresh of a partition
model.Tables["Sales"].Partitions["Sales_Current"].RequestRefresh(RefreshType.Full);
// Process recalc (only recalculate, no data reload)
model.RequestRefresh(RefreshType.Calculate);
// Execute all pending refresh operations
model.SaveChanges();Best Practice Analyzer Rules (Tabular Editor)
Common rules to enforce model quality:
| Rule | Description |
|---|---|
| No implicit measures | All numeric columns should have explicit measures |
| Hide foreign keys | Key columns used in relationships should be hidden |
| Proper naming | Measures should not start with "Sum of" or "Count of" |
| Format strings | All measures should have format strings |
| No bidirectional | Relationships should use single-direction filtering |
| Date table marked | At least one table should be marked as date table |
| Description required | Tables and measures should have descriptions |
| Unused columns | Flag columns not used in any measure, relationship, or visual |
Tooling: TOM, Tabular Editor, fabric-cicd, Fabric CLI, semantic-link, pbi-tools, ALM Toolkit
Reference for the full Power BI programmatic tooling stack: when to pick each tool, setup, and usage examples.
TOM - Tabular Object Model (.NET SDK)
The .NET SDK for creating and managing semantic models programmatically via XMLA endpoint. Use TOM when you need low-level control over the model graph, custom CI/CD tooling, or integration with non-Python .NET applications.
Setup
dotnet add package Microsoft.AnalysisServices.NetCore.retail.amd64
# .NET Framework alternative:
# Install-Package Microsoft.AnalysisServices.retail.amd64Quick Example: Modify an Existing Model
using Microsoft.AnalysisServices.Tabular;
string conn = "DataSource=powerbi://api.powerbi.com/v1.0/myorg/Sales-Dev;" +
"User ID=app:{clientId}@{tenantId};Password={secret};";
using var server = new Server();
server.Connect(conn);
var db = server.Databases.FindByName("SalesModel");
var model = db.Model;
model.Tables["Sales"].Measures.Add(new Measure() {
Name = "YoY Growth",
Expression = @"
VAR Current = [Total Sales]
VAR PY = CALCULATE([Total Sales], SAMEPERIODLASTYEAR('Date'[Date]))
RETURN DIVIDE(Current - PY, PY)",
FormatString = "0.00%"
});
model.SaveChanges();For complete TOM patterns (creating models from scratch, relationships, RLS, OLS, partitions, incremental refresh, perspectives, translations, calculation groups, service principal auth, Azure AD token auth), see tom-advanced-patterns.md.
Licensing Requirement
TOM requires XMLA read/write endpoint access: Premium, PPU, or Fabric F-SKU capacity.
When to Pick TOM vs Alternatives
| Scenario | Use |
|---|---|
| .NET application or custom tooling | TOM |
| Python notebook inside Fabric | semantic-link-labs (wraps TOM) |
| TMDL folder deployment via CLI | Tabular Editor 2 -D switch |
| PBIP project deployment | fabric-cicd |
| Schema diff and selective deploy | ALM Toolkit |
Tabular Editor
External tool for advanced semantic model development. Two editions:
| Feature | TE2 (Free) | TE3 (Paid) |
|---|---|---|
| TOM object editing, C# scripting, BPA | Yes | Yes (enhanced IDE) |
| TMDL read/write | Yes (2.17+) | Full |
| DAX debugger, diagram view, advanced IntelliSense | No | Yes |
| Calculation group selection expressions | No | Yes |
2025-2026 external-tool context: As of June 2025, there are no longer any unsupported write operations for external tools in Desktop -- Tabular Editor, DAX Studio, and ALM Toolkit can freely modify any aspect of the model. The TMDL view (GA in Desktop) further expanded write support for objects that have no UI (calc groups, perspectives, translations, detail row expressions).
C# Script Example: Auto-Generate YTD Measures
foreach (var m in Model.AllMeasures.Where(m => m.DisplayFolder == "Revenue"))
{
var ytd = m.Table.AddMeasure(
m.Name + " YTD",
$"CALCULATE({m.DaxObjectFullName}, DATESYTD('Date'[Date]))"
);
ytd.DisplayFolder = "Revenue\\YTD";
ytd.FormatString = m.FormatString;
}Tabular Editor 2 can deploy TMDL folders to XMLA endpoints via the -D command-line switch, making it a zero-code alternative to fabric-cicd for XMLA-based deployment. See tmdl-mastery skill references for full CLI examples.
fabric-cicd (Python, 2026 Primary Deployment Tool)
fabric-cicd is Microsoft's officially supported, open-source Python library for deploying Fabric items (including PBIP projects) from source control to workspaces. It is the 2026-recommended path for PBIP deployment and is the engine behind the Fabric CLI fab deploy command.
Key facts:
- Package:
pip install fabric-cicd(Python 3.9 - 3.13) - Supports 24 item types, including
SemanticModel,Report,Notebook,DataPipeline,Dataflow,Lakehouse,Warehouse,Environment,VariableLibrary - Automatic dependency ordering (semantic models before reports; lakehouses before dependent notebooks)
- Parameterization via
parameter.ymlfor environment-specific find-and-replace - Orphan cleanup via
unpublish_all_orphan_items() - Authentication via Azure Identity SDK (
InteractiveBrowserCredential,AzureCliCredential,ClientSecretCredential,DefaultAzureCredential,ManagedIdentityCredential)
Minimal deploy.py
import argparse
from azure.identity import InteractiveBrowserCredential, AzureCliCredential
from fabric_cicd import FabricWorkspace, publish_all_items, unpublish_all_orphan_items
parser = argparse.ArgumentParser()
parser.add_argument("--workspace_name", required=True)
parser.add_argument("--environment", default="dev")
parser.add_argument("--spn-auth", action="store_true")
parser.add_argument("--cleanup-orphans", action="store_true")
args = parser.parse_args()
credential = AzureCliCredential() if args.spn_auth else InteractiveBrowserCredential()
target = FabricWorkspace(
workspace_name=args.workspace_name,
environment=args.environment,
repository_directory=".",
item_type_in_scope=["SemanticModel", "Report"],
token_credential=credential,
)
publish_all_items(target)
if args.cleanup_orphans:
unpublish_all_orphan_items(target)Deployment typically takes 20-30 seconds per item. The first deployment requires manually setting data-source credentials in the Fabric portal (Workspace > Semantic Model > Settings > Data source credentials); subsequent deployments reuse them.
Environment Parameterization (parameter.yml)
Place parameter.yml at the project root. fabric-cicd find-and-replaces find_value with the environment-specific replace_value across all PBIP definition files before publishing:
find_replace:
- find_value: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" # dev lakehouse GUID
replace_value:
dev: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
prod: "cccccccc-cccc-cccc-cccc-cccccccccccc"
- find_value: "sql-dev.database.windows.net"
replace_value:
dev: "sql-dev.database.windows.net"
prod: "sql-prod.database.windows.net"fabric-cicd also supports key_value_replace with JSONPath for structured substitution and per-item filters (item_type, item_name, file_path).
Service Principal Requirements
1. Tenant setting "Service principals can call Fabric public APIs" must be enabled in the Fabric admin portal 2. Service principal needs Contributor or Admin role on each target workspace 3. For Direct Lake models, the SP also needs at least Viewer on the source Lakehouse/Warehouse
For complete CI/CD workflow examples (GitHub Actions with OIDC federated credentials, Azure DevOps pipelines, multi-workspace deploy, pre/post hooks, troubleshooting), see fabric-cicd-recipes.md.
Fabric CLI (fab) with fab deploy
Fabric CLI v1.5 (GA March 2026) introduced the `fab deploy` command, which wraps fabric-cicd as a single CLI operation. It accepts the same parameter.yml files you use with the Python library.
# Install
pip install ms-fabric-cli
# Authenticate (once)
fab auth login
# Deploy an entire workspace from a local repo
fab deploy \
--source "./MyProject" \
--workspace "Sales-Dev" \
--environment dev \
--item-types "SemanticModel,Report" \
--cleanup-orphans
# Run from CI/CD with service principal
fab auth login \
--tenant $TENANT_ID \
--service-principal \
--client-id $CLIENT_ID \
--client-secret $CLIENT_SECRET
fab deploy --source . --workspace "Sales-Prod" --environment prodThe CLI also exposes lower-level item management:
fab ls /Sales-Dev/ # list workspace items
fab get /Sales-Dev/SalesModel.SemanticModel # get item definition
fab import /Sales-Dev/SalesModel.Report ./report.pbir # import a single item
fab rm /Sales-Dev/OldReport.Report # delete an itemSemantic Link / semantic-link-labs (Python)
For scripting semantic models from Fabric notebooks (no .NET toolchain required), use:
- `semantic-link` (SemPy) -- preinstalled in Fabric runtimes; read/query models, run DAX, use INFO functions
- `semantic-link-labs` (
sempy_labs) -- Microsoft-maintained higher-level library with a TOM context-manager wrapper, BPA, Direct Lake helpers, and deployment APIs
Minimal TOM wrapper example:
%pip install semantic-link-labs -q
from sempy_labs.tom import connect_semantic_model
with connect_semantic_model(dataset="SalesModel", workspace="Sales-Dev") as tom:
tom.add_measure(
table_name="Sales",
measure_name="Sales Amount",
expression="SUM(Sales[Amount])",
format_string="$ #,##0.00",
display_folder="Revenue",
)
# Changes auto-saved on context exitAlso provides: run_model_bpa, deploy_semantic_model, export_model_to_tmdl, update_direct_lake_model_connection, add_incremental_refresh_policy, set_rls, set_ols, plus direct access to raw TOM via tom.model.
For complete semantic-link-labs recipes (calc groups, incremental refresh, RLS, OLS, Direct Lake, BPA CI gates, TMDL export/import, pythonnet fallback), see the powerbi-master:tmdl-mastery skill's references/tmdl-programmatic-python.md.
pbi-tools
Open-source CLI for extracting, serializing, and compiling PBIX files for source control. Useful for legacy PBIX workflows when PBIP is not yet an option.
2026 status: pbi-tools gained TMDL support starting in 1.0.0-rc.3 and is considered stable for TMDL. Full PBIR support is still evolving -- for new projects, prefer native PBIP + fabric-cicd.
# Extract PBIX to source-control-friendly folder (supports TMDL output)
pbi-tools extract "Report.pbix" -modelFormat TMDL
# Compile back to PBIX
pbi-tools compile "Report/" -format PBIX -outPath "Report.pbix"
# Deploy to Power BI Service
pbi-tools deploy "Report/" -environment ProductionExtracted structure (with TMDL):
Report/
├── .pbixproj.json # Project settings
├── Model/ # TMDL or JSON BIM (configurable)
│ ├── database.tmdl
│ └── tables/
├── Report/ # Report layout (PBIR-Legacy JSON)
│ └── report.json
├── Mashup/ # Power Query M code
│ └── Package/Formulas/
└── StaticResources/ # Images, custom visualsWhen to use pbi-tools vs fabric-cicd:
- pbi-tools: Legacy PBIX files, Report Server, older workflows not yet on PBIP
- fabric-cicd: New PBIP projects, Fabric workspaces, production CI/CD (recommended)
ALM Toolkit
Free tool for schema comparison between semantic models:
- Compare local model vs. published model
- Identify differences in tables, columns, measures, relationships
- Deploy changes selectively
- Works with XMLA endpoint (Premium/PPU/Fabric)
Power BI Desktop Developer Features (2026)
Developer Mode and Git Integration
As of 2026, PBIP save is GA in the Desktop UI. TMDL is the default semantic-model format inside new PBIP projects. PBIR is behind a preview feature toggle until the May 2026 Desktop release:
1. File > Options > Preview features > "Store reports using enhanced metadata format (PBIR)" (still required in April 2026; becomes default in May) 2. File > Save As > Power BI Project (.pbip)
Git integration workflow:
- Save as PBIP locally, commit to Git (TMDL and PBIR files are git-friendly)
- Fabric workspace > Settings > Git integration > connect to Azure DevOps or GitHub repo
- Feature branches for development, PR-based review, auto-sync on merge to main
- Fabric workspace sync is bidirectional: edits in the workspace Service can be committed back to the branch
Important: When connecting a Fabric workspace to Git, semantic models are now exported as TMDL (not TMSL/BIM). Reports are exported in whichever PBIR variant they currently use -- PBIR-Legacy for reports not yet upgraded, PBIR for upgraded reports.
Enhanced Dataset Metadata (GA)
Enhanced metadata format stores semantic model information as text (TMDL) instead of the binary model.bim, enabling:
- Source-control friendly text-based format
- Programmatic editing and diffing
- Better merge conflict resolution
- Compatibility with TMDL and PBIP workflows
Sensitivity Labels in Desktop
Apply Microsoft Purview sensitivity labels directly in Power BI Desktop:
- Labels propagate from datasets to reports and exports
- Mandatory labeling can be enforced via tenant settings
- Labels are preserved when publishing to the service
- Export protection (PDF, PowerPoint, Excel) applies based on label
Note: Sensitivity labels are NOT supported in Power BI Report Server.
Desktop Performance Settings
| Setting | Location | Impact |
|---|---|---|
| Background data | Data Load options | Faster development experience |
| Parallel loading of tables | Data Load options | Faster initial load of multi-table models |
| DirectQuery query timeout | DirectQuery options | Prevent long-running queries |
| Auto date/time | Data Load options | Disable for production (saves memory) |
| Auto recovery | Data Load options | Protect against crashes |
| PBIR format | Preview features | Still required in April 2026; default in May |
| UDFs | Preview features | Enable DAX user-defined functions |
| Enhanced time intelligence | Preview features | Enable calendar-based week functions |