Generated banner for Semantic Model Authoring
Generated logo for Semantic Model Authoring

Semantic Model Authoring

MicrosoftOfficialPublished by Microsoft

  • Skill
  • Tool integration
  • Data and AI
  • MIT
  • No issues found

From the author

Manages Power BI semantic models, including tables, columns, measures, relationships, field parameters, DAX, Import, DirectQuery and Direct Lake designs, sources, connections, permissions, refresh, and deployment. Load this skill before acting on a semantic model, unless it is part of publishing a PBIP project, which powerbi-report-cli owns. For natural-language data questions use fabriciq; for visuals use powerbi-report-cli.
— the description by Microsoft

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The semantic-model-authoring skill provides a structured framework for managing the full lifecycle of Power BI semantic models. It supports creating, inspecting, modifying, and deploying models, including handling DAX, relationships, field parameters, and storage modes like Import, DirectQuery, and Direct Lake. The skill prioritizes the use of the powerbi-modeling-mcp for live model interactions, with fallback procedures for TMDL-based editing in Fabric workspaces or PBIP projects. It enforces strict adherence to best practices for modeling, DAX performance, and AI readiness, ensuring models are robust and optimized for both human and AI-driven consumption.

What it does

  • Creates and configures new semantic models from scratch.
  • Inspects and discovers metadata for tables, columns, measures, and relationships.
  • Modifies existing models by adding or editing objects and refactoring DAX code.
  • Optimizes DAX performance using diagnostic traces and established patterns.
  • Prepares semantic models for AI integration with Microsoft Fabric Copilot and Data Agents.
  • Manages deployment, refresh, and connection binding for models in Fabric workspaces.

When to use it

  • When you need to build or update a semantic model's schema or business logic.
  • When you need to audit a model against performance or modeling best practices.
  • When preparing a model for conversational BI or Copilot experiences.
  • When you need to programmatically bind a model to a specific Fabric data connection.

Advantages

  • Provides a unified, tool-agnostic workflow for diverse model environments.
  • Enforces consistent modeling and DAX coding standards.
  • Prioritizes high-efficiency MCP-based editing to prevent desync issues.
  • Includes specialized workflows for modern features like Direct Lake and Field Parameters.

Usage examples

“Create a new semantic model for my sales data in the 'SalesWorkspace' workspace.”
The agent will guide you through requirements gathering, storage mode selection, star schema design, and deployment to the specified workspace.
“Optimize the 'Total Revenue' measure in my semantic model.”
The agent will use the DAX performance decision guide to diagnose the measure and suggest optimizations based on established performance patterns.
“Prepare my semantic model for use with Fabric Copilot.”
The agent will evaluate your model against the AI readiness checklist and provide actionable steps to improve metadata and business context.
“Bind my semantic model to the 'ProductionGateway' connection.”
The agent will list the model's data sources and execute the necessary API calls to bind them to the specified gateway connection.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/microsoft/semantic-model-authoring/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip semantic-model-authoring.zip -d .agents/skills

Documentation by Microsoft

Telemetry — MANDATORY. Every api.fabric.microsoft.com call must carry x-ms-fabric-skill: semantic-model-authoring (az rest: --headers "x-ms-fabric-skill=semantic-model-authoring"), including every LRO poll, fabric_lro and retry. Snippets omit it — add it anyway.

CRITICAL NOTES

  1. To find the workspace details (including its ID) from workspace name: list all workspaces and, then, use JMESPath filtering

  2. To find the item details (including its ID) from workspace ID, item type, and item name: list all items of that type in that workspace and, then, use JMESPath filtering

  3. Always consider the Tool selection priority when choosing which tool to use for each operation. Do not default to TMDL edits or az rest if MCP is available and connected to the target model.

Power BI Semantic Model Authoring — CLI Skill

Workflow Selector

Use this decision tree to route to the correct workflow based on user intent:

User wants to...Workflow
Create a semantic model from scratchCreate new semantic model
Inspect or discover model metadata (tables, columns, measures, relationships)Discover Semantic Model Metadata
Add/edit semantic model objects (e.g. measures, tables, columns, relationships)Modify an Existing Model
Write or refactor DAX codeModify an Existing Model
Create/edit a field parameterAuthor a Field Parameter
Improve DAX query or measure performanceOptimize DAX Performance
Analyze semantic model against best practicesAnalyze Best Practices
Prepare a semantic model for AI consumption (Copilot / Data Agents)Semantic Model AI Readiness
Deploy a model to a Fabric workspaceDeploy to Fabric
Refresh a semantic modelRefresh Semantic Model
Configure data sources, parameters, or permissionsManage Semantic Model in Fabric
Bind a semantic model to a Fabric connection (or unbind)Bind Semantic Model to a Connection
Export / Get semantic model definition as PBIPExport to PBIP

Table of Contents

Load these references on demand when a workflow step requires them. Do not load all at once.

TopicReferenceWhen to load
Modeling Best Practicesmodeling-guidelines.mdBefore creating or editing any model
Naming Conventionsnaming-conventions.mdWhen naming or renaming tables, columns, measures
Direct Lake Modelingdirect-lake-guidelines.mdWhen model connects to OneLake
TMDL Editingtmdl-guidelines.mdBefore generating or editing any TMDL file
PBIP Projectspbip.mdWhen working with PBIP folders
DAX Languagedax-guidelines.mdWhen writing or reviewing any DAX code
Field Parametersfield-parameters.mdWhen creating/editing a field-parameter table (metric selector, dynamic view)
Metadata Discovery (DAX INFO functions)metadata-discovery.mdWhen discovering model metadata via DAX INFO functions (see Workflow: Discover Semantic Model Metadata)
DAX Performance Decision Guidedax-perf-decision-guide.mdStart here when optimizing DAX
DAX Performance Pattern Catalogdax-perf-patterns.mdLoad on demand after the decision guide identifies candidate patterns
Semantic Model AI Readinesssemantic-model-ai-readiness.mdWhen preparing a model for Copilot or Data Agents
Semantic Model REST APIsemantic-model-rest-api.mdWhen using az rest for TMDL CRUD, refresh, parameters, permissions, or property retrieval
Connection Bindingconnection-binding.mdWhen binding/unbinding a semantic model to a Fabric data connection (gateway, cloud, VNet, automatic, none)
Finding Workspaces/ItemsCOMMON-CLI.mdWhen resolving workspace/item IDs
Fabric Control-Plane APICOMMON-CLI.mdWhen using az rest patterns, LRO, pagination
AuthenticationCOMMON-CLI.mdWhen authenticating with az login
Authentication & Token AcquisitionCOMMON-CORE.md § Authentication & Token AcquisitionWrong audience = 401; read before any auth issue
Core Control-Plane REST APIsCOMMON-CORE.md § Core Control-Plane REST APIsIncludes pagination, LRO polling, and rate-limiting patterns
Definition EnvelopeITEM-DEFINITIONS-CORE.mdWhen building TMDL definition payloads
ExamplesExamplesReference end-to-end walkthroughs.

Tool Selection Priority

Priority order (highest first):

  1. Tier 1 — powerbi-modeling-mcp MCP is registered -> Use MCP for authoring (new or edit) operations against the model from any source: Power BI Desktop, Fabric workspace, or local PBIP folder. MCP is the most reliable and full-featured way to edit semantic models, with immediate effect on the live model and no risk of TMDL desync.

    Important: In case of dynamic search tools is available (e.g. tool_search_tool_regex) search for an available MCP server matching the pattern powerbi-modeling-mcp.

    This includes BOTH writes AND reads/inspection.

    • To inspect or verify changes -> use the corresponding MCP operations (List / Get).

    • Anti-pattern: opening, view-ing, glob-ing or otherwise reading TMDL files (*.tmdl) while MCP is connected. The MCP-loaded model is the source of truth - the on-disk TMDL is stale. The only exceptions is when the user explicitly asks to work with the TMDL files.

  2. Tier 2 — MCP not registered + PBIP folder or Fabric workspace -> Edit TMDL files directly. Load tmdl-guidelines.md and pbip.md. When the source is a Fabric workspace, use az rest to round-trip the TMDL (load semantic-model-rest-api.md): getDefinition -> edit TMDL locally -> updateDefinition.

Fallback — none of the above available (e.g., Power BI Desktop with no PBIP and no MCP) -> STOP. The agent cannot author the model in this configuration. Instruct the user to either (a) install and register the powerbi-modeling-mcp MCP server, or (b) save the PBIX as a PBIP project, then restart the workflow.

All workflows below are tool-agnostic. Workflow steps describe the intent (connect, create, edit, save, deploy, refresh). The tool used to perform each step is determined here. Always select the highest-priority tool available for the current environment; do not mix tools when a higher-priority option works. Some workflows OVERRIDE this default priority, always check the workflow's own tool-selection rules before defaulting to Tier 1.

Connecting to a Semantic Model

A semantic model can live in three locations. Resolve the connection per Tool Selection Priority:

  • Power BI Desktop: Locate the running Power BI Desktop instance and connect to its local model.

  • Fabric workspace: First, find the workspace and semantic model using the Finding Workspaces and Items pattern: list workspaces to resolve the workspace ID by name, then list items of type SemanticModel in that workspace to resolve the model ID by name. Then connect to the model (live) or export its TMDL definition for local editing.

  • PBIP project: Connect to the [Name].SemanticModel/definition folder. Load pbip.md to understand the PBIP folder structure - only load the [Name].SemanticModel/definition folder that includes the TMDL code.

Saving Changes to a Semantic Model

How changes are persisted depends on where the model lives and which tool tier (per Tool Selection Priority) is in use:

Live connection (Tier 1 - MCP against Desktop or Fabric workspace):

  • Changes are applied immediately as each operation executes against the live model. No explicit save step is needed.

  • PBIP project (live via MCP): Serialize the model back to the [Name].SemanticModel/definition folder at the end of the session. If the PBIP folder does not exist yet, follow Export to PBIP to create the full structure first.

Local TMDL editing (Tier 2 - direct file edits or az rest round-trip):

  • PBIP project: Changes are already written to the TMDL files during editing. No additional save step is needed.

  • Fabric workspace: Changes were made to local TMDL files exported from the service. Re-deploy the model (load semantic-model-rest-api.md for the updateDefinition flow) to push changes back to the workspace.


Workflow: Create new Semantic Model

When this applies: User asks to create a new semantic model from scratch.

Steps:

  1. Gather requirements - interview the user until both reach a shared understanding of: purpose of the model, data source connection details and schemas, and key business entities/facts. If data source information is not available, STOP and use ask_user. Do not guess or fabricate.

  2. Determine storage mode - data source is Fabric OneLake -> Direct Lake; otherwise default to Import. Only use DirectQuery when the user explicitly asks for it.

  3. Design star schema - identify fact and dimension tables and relationship keys.

    • If fact table includes date field(s), create a separate date dimension table and link it to the fact with a relationship. If not explicitly requested, use PowerQuery/M partition instead of DAX calculated table.

  4. Load applicable guidelines - MANDATORY: load modeling-guidelines.md before building any object (including the Tier 1 MCP path below); also load direct-lake-guidelines.md if Direct Lake. Don't build from memory.

  5. Build - follow Tool Selection Priority: prefer the modeling MCP to build the whole model when one is available; otherwise use the TMDL code-edit path. Create an empty database (compatibility level 1702+), then for each table follow the execution order from Modify an Existing Model (partitions -> columns -> relationships -> measures). Storage-mode specifics:

    • Import / DirectQuery - create M parameters for the data source (Server, Database, ...) and reference them in partition M code; ensure proper dataType and sourceColumn mapping on columns.

    • Direct Lake - create a shared named expression for the Direct Lake connection using the AzureStorage.DataLake connector; use EntityPartitionSource with directLake mode mapped to the lakehouse table columns.

    Build the whole model in one pass, then deploy once. Add every table, column, relationship and measure before deploying - don't deploy a partial model and edit-redeploy. On the MCP path, keep the model in the session end to end.

  6. Deploy or save - Fabric workspace available -> Deploy to Fabric; otherwise -> Export to PBIP. See Saving Changes to a Semantic Model. Deploy through the same path you built with.

  7. Validate - run Validation Checklist.


Workflow: Discover Semantic Model Metadata

When this applies: User asks to inspect, list, or discover the model's structure - tables, columns, measures, relationships, hierarchies, partitions, roles, or storage internals. Also used internally by other workflows (Modify, Analyze Best Practices, AI Readiness) to inventory the model before editing.

Pick a discovery method (highest priority first):

  1. powerbi-modeling-mcp TOM inspection (List / Get) - the default when powerbi-modeling-mcp is registered and connected to the target model with Write access. It returns the structured object model directly and stays in sync with pending edits, so it is preferred while authoring.

  2. DAX INFO.VIEW functions - query the model's INFO.VIEW.* metadata rowsets. MANDATORY: before writing or running ANY INFO-function DAX, you MUST load metadata-discovery.md first. Do NOT compose INFO queries from memory; load the reference and use its patterns. Prioritize this method when any of the following is true:

    • You lack Write permission on the model. powerbi-modeling-mcp List / Get operations require Write; with Build access, use INFO.VIEW functions executed via the dax_query_operations tool.

    • powerbi-modeling-mcp is not registered or not available. Use Tier 2: export the model definition (getDefinition) and inspect the .tmdl files for schema metadata.

Start narrow: run the scope-estimation and INFO.VIEW.* queries first, then project/filter to only the objects relevant to the task (see metadata-discovery.md).


Workflow: Modify an Existing Model

When this applies: User asks to add/edit/remove measures, tables, columns, relationships, write DAX code, refactor with UDFs, or edit TMDL directly.

Steps:

  1. Connect & discover - per Connecting to a Semantic Model and Discover Semantic Model Metadata. List tables, relationships, existing measures, and identify storage mode (it dictates which guidelines apply).

  2. Load applicable guidelines - modeling-guidelines.md always; direct-lake-guidelines.md if Direct Lake; tmdl-guidelines.md when editing TMDL directly; dax-guidelines.md for any DAX changes (includes UDF refactoring).

  3. Plan changes - identify exactly what to add, modify, or remove. Check for naming conflicts and duplicates.

  4. Execute in correct order:

    • Adding tables - partitions -> columns -> relationships -> measures.

    • Adding relationships - ensure key columns exist on both sides with matching data types;

    • Adding measures - verify referenced columns/tables exist;

  5. Save & validate - per Saving Changes to a Semantic Model and Validation Checklist.


Workflow: Author a Field Parameter

When this applies: User asks to create, edit, reorder, rename, or delete a field parameter. This is a self-contained model-side calculated table; the matching report slicer and visual are separate report work and out of scope here.

Load field-parameters.md before starting and follow the workflow it defines.

Steps:

  1. Connect & discover - per Connecting to a Semantic Model. Detect any existing field parameter (a column carrying ParameterMetadata "kind": 2) before creating a duplicate.

  2. Route to the matching sub-workflow in field-parameters.md:

  3. Save & validate - per Saving Changes to a Semantic Model and Validation Checklist. Confirm the three columns and ParameterMetadata "kind": 2 are intact so Desktop recognizes the table as a field parameter.


Workflow: Optimize DAX Performance

When this applies: User asks to improve DAX query performance, diagnose slow measures, or optimize calculations.

Hard requirement: Requires a trace-capable client (MCP preferred))

Load dax-perf-decision-guide.md first and follow the framework defined there. Load dax-perf-patterns.md only when applying candidate optimization patterns. The framework includes:

  1. Tier model for categorizing optimization effort

  2. Trace diagnostics to identify bottlenecks

  3. Pattern catalog with candidate optimization techniques to test and validate


Workflow: Analyze Best Practices

When this applies: User asks to review, audit, or analyze a semantic model against best practices.

Steps:

  1. Connect & inventory - per Connecting to a Semantic Model and Discover Semantic Model Metadata. Capture all tables, columns, relationships, measures, and storage mode.

  2. Load applicable guidelines - modeling-guidelines.md always; direct-lake-guidelines.md if Direct Lake; naming-conventions.md when assessing naming; dax-guidelines.md when assessing DAX.

  3. Evaluate - compare the model against the loaded guidelines (star schema, naming, relationship cardinality and cross-filter, explicit measures with formatString, column data types and sourceColumn, hidden FK columns, calculated-column-vs-measure choices, Direct Lake constraints, etc.).

  4. Present findings grouped by severity (critical, recommended, optional). For each item state the rule violated and the proposed fix. Wait for user approval.

  5. Apply approved fixes via Modify an Existing Model.

  6. Save & validate - per Saving Changes to a Semantic Model and Validation Checklist.


Workflow: Semantic Model AI Readiness

When this applies: User asks to make a semantic model ready for Microsoft Fabric Copilot, a Power BI Data Agent, or any conversational BI experience. Triggers include "Copilot readiness", "AI readiness", "Prep for AI", "prepare model for Copilot".

Load semantic-model-ai-readiness.md before starting.

Steps:

  1. Confirm scope & gather context - via ask_user, confirm consumption mode (reports only / conversational BI / both) and model stability per the When to Apply section. Collect business context (process, key metrics, common natural-language questions, vocabulary). Do not invent.

  2. Connect & inventory - per Connecting to a Semantic Model. Capture model contents and the source location (PBIP / Fabric workspace / Desktop-only).

  3. Evaluate & route - walk the Readiness Checklist in order; for each gap, classify the fix per Supported Editing Routes (agent-editable TOM metadata vs AI-specific artifacts the user configures in the Power BI "Prep data for AI" UI).

  4. Present findings grouped by severity, each tagged with routing (agent-applicable vs user-action-required). Wait for approval.

  5. Apply approved changes - apply TOM metadata fixes via Modify an Existing Model; for AI instructions, AI Data Schema, and Verified Answers, instruct the user to configure them in the Power BI "Prep data for AI" UI and, only if the user agrees, offer suggestions per the readiness reference; Desktop-only PBIX -> instruct user.

  6. Save, validate, recommend live testing - per Saving Changes to a Semantic Model and Validation Checklist; advise the user to test representative natural-language prompts in Copilot or the Data Agent and iterate.


Workflow: Export to PBIP

When this applies: User asks to export or save a semantic model to a PBIP project folder, or there is no Fabric workspace available to deploy to (e.g., after building a model in-memory).

Key fact: Exporting a model only produces the TMDL definition files. It does NOT create the surrounding PBIP folder structure (Report folder, definition.pbism, definition.pbir, .pbip entry point). The agent must scaffold these before exporting, otherwise the result cannot be opened in Power BI Desktop.

Load pbip.md before starting and follow the PBIP folder structure defined there.

Steps:

  1. Determine target - via ask_user, get the target folder path and the semantic model name. If only a folder is provided, use the model's database name as the semantic model folder name.

  2. Scaffold the PBIP structure - per pbip.md, ensure <Name>.SemanticModel/ (with definition/ and definition.pbism), <Name>.Report/ (with definition/ and definition.pbir using a byPath reference), and <Name>.pbip exist. Create any missing piece.

  3. Export TMDL into <Name>.SemanticModel/definition/, per Tool Selection Priority:

    • Tier 1 (MCP) - use the MCP export/save operation against the live model.

    • Tier 2 (Fabric workspace, no MCP) - call getDefinition (load semantic-model-rest-api.md) and write the returned parts.

    • Local TMDL files already on disk - copy or move them into the definition/ folder.

  4. Validate - confirm the definition/ folder contains at minimum model.tmdl and table .tmdl files; confirm definition.pbism, <Name>.Report/definition.pbir (with correct byPath to ../<Name>.SemanticModel), and <Name>.pbip exist and reference each other correctly.


Workflow: Deploy to Fabric

When this applies: User asks to deploy or publish a semantic model to a Fabric workspace.

Hard rule — this workflow OVERRIDES the default Tool Selection Priority. Do not default to MCP just because it is available. The deployment path is determined by the source of the model, not by which tools are connected. If the source is PBIP/TMDL files on disk, you MUST use the Fabric REST API even when an MCP session is active.

Decision tree (pick exactly one — top-down, first match wins):

  1. Are there PBIP / TMDL files on disk that need to be deployed? -> YES — use Fabric REST API. Call az rest with createItemWithDefinition (new model) or updateDefinition (existing model). Load semantic-model-rest-api.md.

    • Rationale: deploying TMDL files directly via the Fabric API is more reliable, faster, and avoids unnecessarily loading the model into MCP only to push it back out.

    • Do NOT open the PBIP in MCP first and then deploy via MCP. That is an explicit anti-pattern for this workflow.

  2. Is the model already loaded in a live MCP session (e.g., just built in-memory, or currently being edited via MCP) with no PBIP/TMDL files involved? -> Use MCP tool to deploy with the target workspace and semantic model name.

  3. Is the model live in Power BI Desktop with no PBIP saved? -> Use MCP Deploy if MCP is connected to Desktop. If MCP is not available, instruct the user to save as PBIP first, then restart this workflow at step 1.

Verify deployment succeeded by listing workspace items of type SemanticModel.

Deploy exactly once; the deploy is NOT idempotent. A retried deploy creates a second model with the same name, after which the deploy fails with There are multiple datasets named '<name>'. If you hit that error, do NOT retry blindly: list the workspace's semantic models, delete every duplicate with that name, then deploy once from the clean state. If deploying a model that may already exist, delete the existing same-named model(s) before the single deploy.


Workflow: Refresh Semantic Model

When this applies: User asks to refresh data in a semantic model.

Refresh is only possible when working against a live model in Desktop or Fabric Service. If working with local TMDL files, deploy the model first.

Trigger a refresh per Tool Selection Priority:

  • Power BI Desktop: Tier 1 (MCP) only — use the MCP Refresh operation.

  • Fabric Service: Tier 1 (MCP Refresh operation) or fallback to the Power BI Enhanced Refresh API (load semantic-model-rest-api.md).

If the refresh fails with a credential error, stop immediately and instruct the user to configure the data source connections manually in Power BI Service. Do not attempt to retry or work around credential errors programmatically.


Workflow: Manage Semantic Model in Fabric

When this applies: User asks to configure data sources, update parameters, or manage permissions for a semantic model in Fabric Service.

Hard rule — this workflow OVERRIDES the default Tool Selection Priority. Do not default to MCP (Tier 1) prefer using az rest and REST APIs.

Data Sources & Parameters

Get/update data sources and parameters via Power BI REST API. Load semantic-model-rest-api.md.

Permissions

List/grant/update dataset user permissions via Power BI REST API. Load semantic-model-rest-api.md.

Connection Binding

Load connection-binding.md and follow it. The reference covers prerequisites, the bindConnection endpoint, the discover -> match -> bind -> validate steps, all connectivityType values, the unbind pattern, and troubleshooting.

Key rules (full details in the reference):

  • Use the Fabric Bind Semantic Model Connection REST API (supersedes the legacy Power BI BindToGateway).

  • One bind request per data source reference - the API does not support bulk binding.

  • Discover the model's data source references via List Item Connections, then match connectionDetails against List Connections to find the target id. Create a connection first if no match exists.

  • Validate by re-listing item connections and triggering a refresh.


Validation Checklist

Run after any model creation or modification:

Always (works with PBIP, Desktop, and Fabric Service):

  1. Check the PBIP structure - if the model is sourced from a PBIP folder, ensure the folder structure and files are correct (see pbip.md).

  2. Verify against modeling guidelines - re-check every change against modeling-guidelines.md (and direct-lake-guidelines.md for Direct Lake models).

Only when connected to an Analysis Services database (Power BI Desktop or Fabric Service):

  1. Test new measures - for each new measure, run a simple DAX query to validate it returns expected results (e.g., EVALUATE { [Measure Name] }). Skip this step when working with local TMDL/PBIP files only.

  2. Test table refresh - when new tables were created, trigger a refresh to verify that partitions, data source expressions, and column mappings are correct. A failed refresh typically indicates mismatched sourceColumn names, invalid M expressions, or incorrect Direct Lake entity references. Skip this step when working with local TMDL/PBIP files only.

If any check fails, fix the issue and re-run validation.


Must/Prefer/Avoid

MUST
  • Understand the data source schema before starting - analyze source tables, columns, and data types before designing or modifying the model.

  • Follow modeling guidelines - load modeling-guidelines.md before creating or editing any model; apply star schema design, naming conventions, and column/measure rules

  • Follow Tool Selection Priority - always pick the highest-priority tool tier available for the current environment; do not mix tiers when a higher-priority option works

PREFER
  • Star schema over snowflake or flat tables - denormalized dimensions with single-column relationship keys

  • Consistency with existing model patterns - when editing an existing model, match its naming conventions and structure rather than imposing new ones

  • TMDL format over TMSL - text-based, diff-friendly, preferred for Fabric

  • Validate after changes - use Workflow: Discover Semantic Model Metadata to confirm objects landed.

AVOID
  • Hardcoded workspace/item IDs - resolve dynamically via API

  • Reading TMDL files when MCP is connected - view/glob on *.tmdl while a Tier 1 MCP session is live is an anti-pattern (see Tool Selection Priority).

  • Hand-authoring TMDL files when MCP is registered - using create/edit/file-write tools to scaffold model.tmdl, database.tmdl, relationships.tmdl, or tables/*.tmdl is a Tier 1 anti-pattern, including for brand-new models. Build and export via MCP tools.

DENY
  • Manage RLS/OLS role membership - do not propose REST calls, az rest URLs, MCP operations, or TMDL changes to add/remove users or groups from a security role. Refuse the request as out-of-scope here and redirect the user to the Power BI portal.


Examples

Scope note — examples use az rest for discovery to resolve ID's and discover Fabric metadata (see COMMON-CLI.md § Finding Workspaces and Items). Authoring of the semantic model definition is routed through Tool Selection Priority: Tier 1 MCP powerbi-modeling-mcp when available, Tier 2 TMDL editing via getDefinition / updateDefinition otherwise.

Example 1: Modify an Existing Semantic Model

Prompt: "Create base measures for all aggregable columns in the semantic model Sales in workspace Marketing."

Agent response - follows Workflow: Modify an Existing Model.

  1. Discover IDs via az rest.

  2. Connect to the model per Connecting to a Semantic Model and Tool Selection Priority. With powerbi-modeling-mcp registered, connect MCP directly to the Fabric workspace model (Tier 1). Otherwise fall back to Tier 2 (getDefinition -> edit TMDL locally).

  3. Inspect & plan - list tables and columns via the active tool tier; identify aggregable columns (numeric, not foreign keys, not hidden surrogate IDs) and decide on SUM / AVERAGE / MIN / MAX per column following modeling-guidelines.md and naming-conventions.md. Load dax-guidelines.md before writing DAX.

  4. Add measures per Workflow: Modify an Existing Model.

    • Tier 1 (MCP): call tool create for each new measure with expression, formatString, and target table. Do not hand-author TMDL.

    • Tier 2 (no MCP): edit the table's .tmdl file directly and round-trip via updateDefinition REST API.

  5. Save & validate per Saving Changes to a Semantic Model.


Example 2: Create a New Semantic Model from a Fabric Lakehouse

Prompt: "Create a new Power BI semantic model in workspace Marketing, using the SalesLakehouse in the same workspace as the data source."

Agent response - follows Workflow: Create new Semantic Model.

  1. Discover workspace + lakehouse IDs via az rest.

  2. Discover lakehouse schema - list tables and columns via the Lakehouse OneLake APIs or the SQL Endpoint of the lakehouse. Capture table names, column names, and data types - these drive semantic model table, column definitions and partition metadata.

  3. Select tool tier per Tool Selection Priority: prefer Tier 1 (powerbi-modeling-mcp) for the full build. Tier 2 (PBIP/TMDL scaffolding then deploy) only if MCP is not registered.

  4. Build the model following Workflow: Create new Semantic Model - data source is OneLake so the storage mode is Direct Lake (load direct-lake-guidelines.md and modeling-guidelines.md). Design a star schema from the lakehouse tables, then:

    • Create the empty database.

    • Create one shared named expression for the lakehouse connection.

    • For each table: add EntityPartitionSource (mode directLake) -> columns mapped to lakehouse columns -> relationships -> measures.

  5. Deploy per Workflow: Deploy to Fabric:

  6. Validate per the Validation Checklist - confirm the model appears in the workspace and trigger an initial refresh to verify Direct Lake column mappings resolve.


Example 3: Bind a Semantic Model to a Fabric Connection

Prompt: "Bind semantic model Sales in workspace Marketing to the sql-prod connection."

Agent response - follows Workflow: Manage Semantic Model in Fabric § Connection Binding. No MCP - this workflow overrides Tool Selection Priority and is REST-only.

  1. Discover workspace + model IDs via az rest.

  2. Discover the target connection in Fabric by matching the server name:

    SERVER="sql-prod"
    CONN_ID=$(az rest --method get --resource "https://api.fabric.microsoft.com" \
      --url "https://api.fabric.microsoft.com/v1/connections" \
      --query "value[?connectionDetails.path | contains(@, '$SERVER')] | [0].id" -o tsv)

    If no connection matches, stop and instruct the user to create one first per connection-binding.md.

  3. Execute the bind per Workflow: Manage Semantic Model in Fabric § Connection Binding - load connection-binding.md and follow the discover -> match -> bind -> validate flow:

    • List the model's data source references (List Item Connections).

    • For each reference whose connectionDetails matches $CONN_ID, call the Fabric bindConnection endpoint one request per data source reference (no bulk binding).

  4. Validate - re-list item connections to confirm the binding, prompt the user to trigger a refresh per Workflow: Refresh Semantic Model. Credential errors -> stop and direct the user to the Service portal (per TROUBLESHOOTING).


TROUBLESHOOTING

SymptomFix
MCP connection failureFall back to TMDL editing (see Tool Selection Priority). Inform the user about the fallback.
TMDL validation errorsRead error details, fix syntax, re-validate. Load tmdl-guidelines.md.
403 Forbidden / identity NoneUser needs Contributor+ role - stop immediately. Do not retry.
401 UnauthorizedCorrect the --resource audience once (see semantic-model-rest-api.md). If it persists after the audience is right, it is missing permissions - stop and tell the user. Do not retry-loop.
202 Accepted but no resultPoll LRO to completion.
Parts missing after updateDefinitionMust include ALL parts - modified + unmodified.
Refresh credential errorDirect user to configure in Service portal. Do not retry.
DAX errors in measuresCheck column/table name references (case-sensitive). Verify referenced objects exist.
Deployment failureCheck workspace permissions, model compatibility level, and Direct Lake expression source references.
Missing data sourceVerify M parameters or named expressions are correctly defined.

Files

references/dax-perf-patterns.md

DAX Performance Pattern Catalog

Use this on demand after dax-perf-decision-guide.md identifies candidate patterns. Patterns are candidate optimizations, not guarantees; validate trace/runtime and semantic equivalence before keeping changes.

Section 3: Tier 1 DAX Optimization Patterns

Autonomy: Auto-apply freely. Modify only measure/UDF definitions in the DEFINE block. Keep EVALUATE and SUMMARIZECOLUMNS grouping identical.

Candidate optimizations, not guarantees: Each pattern is a hypothesis to test. Cardinality, data layout, relationships, filters, storage mode, and engine folding can make a rewrite help, do nothing, or hurt. Apply one or more matching patterns, validate trace/runtime and semantic equivalence after each step, and continue iterating because one rewrite can expose the next optimization opportunity. Keep changes that help; revert changes that do not.

Prefer SUMMARIZECOLUMNS: Fully supported inside measure definitions — earlier restrictions no longer apply. Use it to replace ADDCOLUMNS/SUMMARIZE patterns (DAX002), pre-materialize context transitions before iterating (DAX006), and cache repeated evaluations into a single virtual table (DAX003). Prefer it over ADDCOLUMNS(VALUES(...), ...) unless a specific scenario prevents it.

DAX001: Use Simple Column Filter Predicates as CALCULATE Arguments

Identifier: FILTER(Table, ...) as a filter argument, or combined predicates. Verify trace; simple cases can fold to WHERE.

Action: Keep filters column-scoped. Replace table filters with boolean predicates, and split && into separate filter arguments.

Anti-pattern — FILTER with table expression uses an iterator:

CALCULATE(
    SUM('Sales'[Amount]),
    FILTER('Product', 'Product'[Category] = "Electronics")
)

Preferred — column predicate, no iterator:

CALCULATE(
    SUM('Sales'[Amount]),
    KEEPFILTERS( 'Product'[Category] = "Electronics")
)

Anti-pattern — && joins predicates into a single iterator argument:

CALCULATETABLE( 'Sales', 'Sales'[Region] = "West" && 'Sales'[Amount] > 1000 )

Preferred — separate predicates for better query plan:

CALCULATETABLE( 'Sales', 'Sales'[Region] = "West", 'Sales'[Amount] > 1000 )

DAX002: Use SUMMARIZECOLUMNS for Grouped Calculations

Identifier: grouped calculation built with ADDCOLUMNS/SUMMARIZE. Compare trace against a direct SUMMARIZECOLUMNS shape.

Action: Use SUMMARIZECOLUMNS for grouping + calculation when row shape and filter semantics stay identical.

Anti-patterns:

SUMMARIZE ( 'Sales', 'Sales'[ProductKey], "Total Profit", [Profit] )
ADDCOLUMNS ( SUMMARIZE ( 'Sales', 'Sales'[ProductKey] ), "Total Profit", [Profit] )
ADDCOLUMNS ( 'Sales', "Total Profit", CALCULATE ( [Profit] ) )
ADDCOLUMNS ( VALUES('Sales'[ProductKey]), "Total Profit", [Profit] )

Preferred:

SUMMARIZECOLUMNS ( 'Sales'[ProductKey], "Total Profit", [Profit] )

DAX003: Cache Repeated Expressions in Variables

Identifier: repeated measure or expression references. Verify with FE time, repeated SE requests, or cache matches.

Action: Cache repeated or row-independent expressions in variables at the smallest safe scope.

Anti-pattern — repeated measure reference:

VAR TotalA = [Sales Amount] * 1.1
VAR TotalB = [Sales Amount] * 0.9
VAR TotalC = [Sales Amount] + 1000

Preferred:

VAR _SalesAmount = [Sales Amount]
VAR TotalA = _SalesAmount * 1.1
VAR TotalB = _SalesAmount * 0.9
VAR TotalC = _SalesAmount + 1000

Anti-pattern — same measure iterated twice:

VAR A = SUMX ( VALUES('Sales'[ProductKey]), [Total Sales] )
VAR B = AVERAGEX ( VALUES('Sales'[ProductKey]), [Total Sales] )

Preferred — materialize once:

VAR Base = SUMMARIZECOLUMNS ( 'Sales'[ProductKey], "@TotalSales", [Total Sales] )
VAR A = SUMX ( Base, [@TotalSales] )
VAR B = AVERAGEX ( Base, [@TotalSales] )

Anti-pattern — context-independent expression inside iterator:

SUMX( 'Sales', 'Sales'[Quantity] * [Average Price] * 1.1 )
// [Average Price] doesn't change per row

Preferred:

VAR _AvgPrice = [Average Price]
RETURN SUMX( 'Sales', 'Sales'[Quantity] * _AvgPrice * 1.1 )

DAX004: Remove Redundant Filters

Identifier: repeated predicates, duplicate filter tables, or redundant key-set filters.

Action: Keep one copy of each filter. Remove key sets or variables that restate an active predicate.

Anti-pattern — same predicate in CALCULATE + FILTER:

CALCULATE(
    SUM('Sales'[Amount]),
    'Sales'[Year] = 2023,
    FILTER('Sales', 'Sales'[Year] = 2023)
)

Anti-pattern — redundant filter variable:

VAR FilteredValues = CALCULATETABLE ( DISTINCT ( 'Table'[Key1] ), 'Table'[Amount] > 1000 )
VAR Result =
    CALCULATETABLE (
        SUMMARIZECOLUMNS ( 'Table'[Key2], "TotalQty", SUM ( 'Table'[Quantity] ) ),
        'Table'[Amount] > 1000,
        'Table'[Key1] IN FilteredValues  -- redundant: already filtered by Amount > 1000
    )

Preferred — single filter, no duplication:

CALCULATE( SUM('Sales'[Amount]), 'Sales'[Year] = 2023 )

VAR Result =
    CALCULATETABLE (
        SUMMARIZECOLUMNS ( 'Table'[Key2], "TotalQty", SUM ( 'Table'[Quantity] ) ),
        'Table'[Amount] > 1000
    )

DAX005: Move Complex SUMMARIZE Inputs to CALCULATETABLE

Identifier: SUMMARIZE starts from a filtered or computed table expression.

Action: Keep grouping simple; move filters to an outer CALCULATETABLE.

Anti-pattern:

SUMMARIZE(
    CALCULATETABLE('Sales', 'Sales'[Year] = 2023, 'Customer'[Segment] = "Enterprise"),
    'Sales'[CustomerKey],
    "DistinctStores", DISTINCTCOUNT('Sales'[StoreKey])
)

Preferred:

CALCULATETABLE(
    SUMMARIZECOLUMNS(
        'Sales'[CustomerKey],
        "DistinctStores", DISTINCTCOUNT('Sales'[StoreKey])
    ),
    'Sales'[Year] = 2023,
    'Customer'[Segment] = "Enterprise"
)

DAX006: Precompute Iterator Inputs with SUMMARIZECOLUMNS

Identifier: iterator over VALUES(...) with CALCULATE or measure evaluation per row.

Action: Precompute iterator rows and values with SUMMARIZECOLUMNS, then iterate the materialized result.

Anti-pattern:

SUMX(
    VALUES('Product'[Attribute]),
    CALCULATE(SUM('Sales'[Amount]))
)

Preferred:

SUMX(
    SUMMARIZECOLUMNS(
        'Product'[Attribute],
        "@Amount", SUM('Sales'[Amount])
    ),
    [@Amount]
)

DAX007: Convert Boolean Tests Without IF

Identifier: row-by-row IF/SWITCH boolean conversion. Trace may show CallbackDataID when it cannot fold.

Action: Convert boolean-to-1/0 tests without IF; use a predicate + COUNTROWS when counting rows.

Anti-pattern:

SUMX(
    'Products',
    IF([Sales Amount] > 10000000, 1, 0)
)

Preferred:

SUMX(
    'Products',
    INT([Sales Amount] > 10000000)
)

Count rows case: eliminate the iterator and callback with a simple predicate.

-- Anti-pattern: iterator + conditional = callback
SUMX( 'Sales', IF('Sales'[Amount] > 1000, 1, 0) )

-- Preferred: native SE aggregation, no iterator, no callback
CALCULATE( COUNTROWS('Sales'), 'Sales'[Amount] > 1000 )

DAX008: Context Transition in Iterator

Identifier: measure reference or CALCULATE inside an iterator. Verify with FE time or repeated short SE events.

Action: Reduce or remove iterator context transitions:

  1. Remove it completely:

// Instead of: SUMX( 'Sales', [Sales Amount] )
// Use: SUMX( 'Sales', 'Sales'[Unit Price] * 'Sales'[Quantity] )
  1. Iterate over a narrow key table:

// Instead of: SUMX( 'Customer', [Total Sales] )
// Use: SUMX( VALUES('Customer'[CustomerKey]), [Total Sales] )
  1. Reduce cardinality before iteration:

// Instead of: SUMX( 'Customer', [Total Sales] * 'Customer'[DiscountRate] )
// Use only when grouping customers by DiscountRate preserves the result:
SUMX(
    VALUES('Customer'[DiscountRate]),
    [Total Sales] * 'Customer'[DiscountRate]
)

DAX009: Externalize SUMMARIZECOLUMNS Filters

Identifier: filter arguments inside SUMMARIZECOLUMNS. Verify materialization and result shape.

Action: Move filters to a wrapping CALCULATETABLE.

Anti-pattern:

SUMMARIZECOLUMNS (
    'Table'[Column],
    TREATAS ( { "Value" }, 'Table'[FilterColumn] ),
    "@Calculation", [Measure]
)

Preferred:

CALCULATETABLE (
    SUMMARIZECOLUMNS (
        'Table'[Column],
        "@Calculation", [Measure]
    ),
    'Table'[FilterColumn] = "Value"
)

DAX010: Push Table Filters with CALCULATETABLE

Identifier: standalone FILTER(...) table where filter context could be applied directly. Simple cases can fold to WHERE.

Action: Use CALCULATETABLE so the filter applies before the table is consumed.

Anti-pattern:

FILTER( 'Sales', 'Sales'[Year] = 2023 )

Preferred:

CALCULATETABLE( 'Sales', 'Sales'[Year] = 2023 )

DAX011: Test DISTINCTCOUNT Alternatives

Identifier: DISTINCTCOUNT; trace cue is DCOUNT, especially when cells have different filter sets.

Action: Benchmark DISTINCTCOUNT against SUMX(DISTINCT(), 1); keep only if equivalent and faster for the target visual.

Storage Engine Bound:

DISTINCTCOUNT('Sales'[CustomerKey])

Formula Engine Bound (sometimes faster):

SUMX(DISTINCT('Sales'[CustomerKey]), 1)

DAX012: Preserve Filters Deliberately

Identifier: ALLEXCEPT or REMOVEFILTERS/ALL + VALUES used to preserve filters. Validate across report groupings.

Action: Choose the preservation form that matches how the retained value is filtered.

Use ALLEXCEPT only when the preserved column is already directly filtered:

CALCULATE( [Total Sales], ALLEXCEPT('Sales', 'Sales'[Region]) )

Keep ALL/REMOVEFILTERS + VALUES when the preserved value may be cross-filtered by another column:

CALCULATE( [Total Sales], REMOVEFILTERS('Sales'), VALUES('Sales'[Region]) )

DAX013: Keep Branch Measures SE-Friendly

Identifier: SWITCH/IF chooses between measure branches. Verify FE time and unused branch work in trace/query plan.

Action checklist:

  • Read the selector column directly filtered by the slicer/query; avoid hidden sort keys unless filtered directly.

  • Keep branches SE-native: one simple aggregation or one fact iterator with row-level arithmetic, not multiple separate aggregations.

  • Use one numeric type across branches; cast mixed branches explicitly.

  • Avoid iterator context transition; lift row-independent measures into variables.

For disconnected parameter tables, prefer field parameters or aligning the selector/filter column before making model metadata changes.


DAX014: Use COUNTROWS for Recognized Keys

Identifier: DISTINCTCOUNT over a recognized unique key. Trace may compile to COUNT; verify before rewriting.

Action: Prefer COUNTROWS only when the counted column is a recognized unique key.

  • Safe candidates: table key, or one-side key of a regular active relationship.

  • Test first: inactive relationships, USERELATIONSHIP, or unmarked keys.

  • Non-key/high-cardinality bottleneck → see DAX011.


DAX015: Iterate at the Required Grain

Identifier: iterator scans more rows than a repeated measure/context-transition dependency requires. Do not use for simple SE-native column sums.

Action: Iterate the lowest distinct grain only when it removes repeated measure or context-transition work.

Anti-pattern:

-- 100K customers but only 5 distinct DiscountRate values → 100K context transitions
SUMX( 'Customer', CALCULATE(SUM('Sales'[Amount])) * 'Customer'[DiscountRate] )

Preferred:

-- 5 iterations instead of 100K
SUMX( VALUES('Customer'[DiscountRate]), CALCULATE(SUM('Sales'[Amount])) * 'Customer'[DiscountRate] )

DAX016: Test Relationship Overrides Locally

Identifier: bidirectional/M2M filter path, or TREATAS/CROSSFILTER override. Verify joins in xmSQL.

Action: Test alternate filter propagation in the measure with TREATAS/CROSSFILTER before changing the model.

Example — replace bidirectional bridge with explicit filter:

CALCULATE(
    SUM('Sales'[Amount]),
    CROSSFILTER('Customer'[CustomerKey], 'CustomerBridge'[CustomerKey], NONE),
    TREATAS(VALUES('CustomerBridge'[CustomerKey]), 'Customer'[CustomerKey])
)

DAX017: Align Scan Shape with Boolean Multipliers

Identifier: sibling measures differ only by per-measure filters on the same fact. Trace may show near-identical SE scans differing only by WHERE values.

Action: Move the per-measure filter into a boolean multiplier so competing SE scans can share shape; verify fusion in trace.

-- Anti-pattern: separate SE query per measure
CALCULATE( SUM('Sales'[Amount]), 'Product'[Category] = "Bikes" )
CALCULATE( SUM('Sales'[Amount]), 'Date'[Date] = _dateAnchor )
CALCULATE( MAX('Sales'[DateKey]),  'Sales'[Metric] <> 0 )

-- Fix: boolean multiplier — structurally similar SE queries can fuse; verify in trace
SUMX( KEEPFILTERS(ALL('Product'[Category])), CALCULATE(SUM('Sales'[Amount])) * ('Product'[Category] = "Bikes") )
SUMX( KEEPFILTERS(ALL('Date'[Date])),        CALCULATE(SUM('Sales'[Amount])) * ('Date'[Date] = _dateAnchor) )
MAXX( ALL('Date'[Date]),                     CALCULATE(MAX('Sales'[DateKey])) * INT(NOT ISBLANK(CALCULATE(SUM('Sales'[Metric])))) )
  • KEEPFILTERS preserves external context; column in the groupby → detail cells iterate 1 row.

  • Best for additive (SUM-style) aggregations.

  • Validate MIN/MAX/AVERAGE: injected 0 values can corrupt the result.

  • BLANK caveat: returns 0 instead of BLANK when no data exists; wrap if downstream ISBLANK() checks matter.


DAX018: Keep Iterator Division SE-Native

Identifier: DIVIDE() inside an iterator. Trace may show CallbackDataID when it cannot fold.

Action: Use / only when the denominator is known non-zero; otherwise pre-filter zero denominators first.

Anti-pattern:

SUMX('Fact', 'Fact'[BaseAmount] * DIVIDE(RELATED('Items'[Discount]), RELATED('Items'[LocationAdjustment])))

Preferred:

SUMX('Fact', 'Fact'[BaseAmount] * (RELATED('Items'[Discount]) / RELATED('Items'[LocationAdjustment])))

DAX019: Move Time Windows Outside Sibling Measures

Identifier: sibling measures each apply time-window filters. Verify whether trace shows separate SE scans.

Action: Keep base measures time-window free; apply TI once in the outer CALCULATE.

Custom time predicates: CALCULATE(expr, Column = _var) does not match this rule; see DAX017.

Anti-pattern — each measure applies TI independently (no fusion):

MEASURE 'Sales'[Revenue YTD] = CALCULATE ( [Revenue], DATESYTD('Date'[Date]) )
MEASURE 'Sales'[Cost YTD]    = CALCULATE ( [Cost],   DATESYTD('Date'[Date]) )
MEASURE 'Sales'[Margin YTD] =
    [Revenue YTD] - [Cost YTD]

Preferred — base measures fuse, TI applied once:

MEASURE 'Sales'[Margin YTD] =
    CALCULATE ( [Revenue] - [Cost], DATESYTD ( 'Date'[Date] ) )

DAX020: Keep Slice Measures Fusion-Friendly

Identifier: slice measures differ only by simple filters or dynamic values. Verify whether trace fuses or separates scans.

Action: Keep slice measures simple and literal; lift time-intelligence or dynamic filters to the combining measure.

Anti-pattern — TI inside each slice measure (no fusion):

MEASURE 'Sales'[Bikes YTD]       = CALCULATE ( SUM('Sales'[Amount]), 'Product'[Category] = "Bikes",       DATESYTD('Date'[Date]) )
MEASURE 'Sales'[Accessories YTD] = CALCULATE ( SUM('Sales'[Amount]), 'Product'[Category] = "Accessories", DATESYTD('Date'[Date]) )

Preferred — slice measures fuse, TI applied once:

MEASURE 'Sales'[Bikes]       = CALCULATE ( SUM('Sales'[Amount]), 'Product'[Category] = "Bikes" )
MEASURE 'Sales'[Accessories] = CALCULATE ( SUM('Sales'[Amount]), 'Product'[Category] = "Accessories" )
MEASURE 'Sales'[Combined YTD] = CALCULATE ( [Bikes] + [Accessories], DATESYTD('Date'[Date]) )
  • Variable-driven slicers: leave base measures filter-free; put the dynamic predicate on the outer measure.

  • Sliced column not in the groupby → see DAX017.


DAX021: Join Precomputed Key Sets in FE

Identifier: computed key set re-filters the same fact via TREATAS/IN. Trace cue is large IN/INB; ININDEX or compound tuples can also appear.

Action: Precompute both aggregations at the shared key grain, then join in FE.

  • Avoid pushing computed key sets back to the fact scan with TREATAS/IN.

  • Both sides must keep a shared lineage column for NATURALINNERJOIN.

Anti-pattern — TREATAS pushes key set back to SE, compounded by outer groupby:

VAR _FilteredAgg =
    CALCULATETABLE (
        ADDCOLUMNS ( VALUES ( 'Fact'[Key] ), "@Agg1", [Measure] ),
        'Dim'[Filter] = "X"
    )
VAR _Qualifying = FILTER ( _FilteredAgg, [@Agg1] > 1000000 )
VAR _Result =
    CALCULATE (
        [Measure],
        TREATAS ( SELECTCOLUMNS ( _Qualifying, "K", 'Fact'[Key] ), 'Fact'[Key] )
    )

Preferred — both aggregations pre-computed, joined in FE:

VAR _FilteredAgg =
    CALCULATETABLE (
        ADDCOLUMNS ( VALUES ( 'Fact'[Key] ), "@Agg1", [Measure] ),
        'Dim'[Filter] = "X"
    )
VAR _Qualifying = FILTER ( _FilteredAgg, [@Agg1] > 1000000 )
VAR _UnfilteredAgg =
    ADDCOLUMNS ( VALUES ( 'Fact'[Key] ), "@Agg2", [Measure] )
VAR _Joined = NATURALINNERJOIN ( _Qualifying, _UnfilteredAgg )
VAR _Result = SUMX ( _Joined, [@Agg2] )

Goal: replace the final key-set fact scan with precomputed tables and an FE join. Keep only if trace/runtime improves.


Section 4: Tier 2 Query Structure Patterns

STOP — Requires user approval before applying any change. Explain the impact on query output and wait for explicit confirmation.

Scope: Desktop-Achievable Changes Only

Every Tier 2 recommendation must map to an action the report author can perform in Power BI Desktop's UI. The agent optimizes the generated DAX query, but the user implements changes through the Desktop interface — not by editing DAX directly in the query pane. Examples of valid changes:

  • Changing the axis/groupby field (e.g., swap Calendar Date for Calendar Month on a visual axis)

  • Removing or adding visual-level filters (e.g., drop an unneeded slicer selection)

  • Changing filter values (e.g., narrow a date range filter)

  • Removing measure value filters (e.g., remove a "Top N" or "> threshold" filter from a visual)

  • Changing aggregation type on a column (e.g., Sum → Average)

QRY001: Remove Unneeded Filters

Every filter adds a WHERE clause in xmSQL and may force an extra SE join. Users often apply slicer or visual-level filters that don't affect the calculation being optimized.

Detection: WHERE clauses on columns not used in the measure logic, or filter variables that restrict to a single value (e.g., Currency[Code] = "USD" in a USD-only model).

Fix: Remove filters one at a time and re-run; if the result doesn't change, the filter is unneeded. Filters needed across all visuals → push to the data source (model-level — see Section 5).

-- Before: filter on Currency adds an SE join for no benefit
SUMMARIZECOLUMNS (
    'Product'[Category],
    KEEPFILTERS ( TREATAS ( {"USD"}, 'Currency'[Code] ) ),
    "Revenue", [Total Revenue]
)

-- After: filter removed, same result, one fewer SE join
SUMMARIZECOLUMNS ( 'Product'[Category], "Revenue", [Total Revenue] )

QRY002: Eliminate Report Measure Filters (__ValueFilterDM)

When a visual filters on a measure value (e.g., "Revenue > 1M"), Power BI generates a __ValueFilterDM variable that can evaluate the measure twice — once for the filter check, once for display.

Detection: __ValueFilterDM in the generated query.

Fix: Move the threshold into the measure itself — return BLANK below the cutoff. SUMMARIZECOLUMNS auto-drops blank rows, achieving the same visual result in one pass:

MEASURE 'Sales'[Total Revenue Filtered] =
    VAR __Rev = [Total Revenue]
    RETURN IF ( __Rev > 1000000, __Rev )

QRY003: Reduce Query Grain

Grouping by a high-cardinality column (e.g., Calendar[Date] → 365 rows) when the user only needs monthly data (12 rows) inflates SE row count ~30×.

Detection: Groupby on a date or high-cardinality column producing far more rows than the visual needs.

Option A — coarser groupby:

-- Daily → monthly
SUMMARIZECOLUMNS ( 'Calendar'[YearMonth], "Revenue", [Total Revenue] )

Option B — period-end axis + measure pin (show period-end snapshot instead of full-period aggregate):

Requires a period-end column in the date table (e.g., Calendar[MonthEndDate]). User changes the visual axis to it, then pins the measure to that date:

-- User changes axis from Calendar[Date] to Calendar[MonthEndDate]
-- Measure pins CALCULATE to the period-end date to return that day's value only
MEASURE 'Sales'[Active Customers] =
    CALCULATE (
        DISTINCTCOUNT ( 'Sales'[CustomerID] ),
        'Calendar'[Date] = MAX ( 'Calendar'[MonthEndDate] )
    )

Without the pin, grouping by MonthEndDate aggregates all days in the month instead of returning the single-day value.

Option C — return BLANK for non-boundary dates (keeps all dates in groupby but only computes on end-of-month):

MEASURE 'Sales'[Revenue EOM] =
    IF ( MAX('Calendar'[Date]) = EOMONTH(MAX('Calendar'[Date]), 0), [Total Revenue] )

Option D — daily additive measure approximated at coarser grain (divide monthly total by days in month):

MEASURE 'Sales'[Daily Avg Revenue] =
    DIVIDE (
        [Total Revenue],
        DAY ( EOMONTH ( MAX('Calendar'[Date]), 0 ) )
    )

QRY004: Remove BLANK Suppression (Changes Result Shape)

+ 0, IF(ISBLANK([M]), 0, [M]), or COALESCE(..., 0) force SUMMARIZECOLUMNS to evaluate every groupby combination — including rows with no data — inflating the result set.

Detection: + 0, IF(ISBLANK(...)), or COALESCE(..., 0) appended to measures.

Anti-pattern:

MEASURE 'Sales'[Revenue] = SUM ( 'Sales'[SalesAmount] ) + 0

Preferred:

MEASURE 'Sales'[Revenue] = SUM ( 'Sales'[SalesAmount] )

If zeros are required selectively, conditionally add 0 where it makes sense:

MEASURE 'Sales'[Revenue] =
    VAR _ForceZero = NOT ISEMPTY ( 'Sales' )
    RETURN [Sales Amount] + IF ( _ForceZero, 0 )

Section 5: Tier 3 Model Optimization Patterns

STOP — Requires user approval before applying any change. Warn that model changes can break downstream reports. Suggest working on a model copy. Apply changes through whatever semantic-model authoring path is already in use; upstream source changes (Lakehouse, Warehouse, Power Query) must be coordinated with the user's data engineering or pipeline workflow.

General Data Layout Best Practices

Data layout decisions affect performance at the source level — before DAX, before the SE. Apply after exhausting DAX and query structure optimizations; changes here require ETL or pipeline modifications. Apply to both Import and Direct Lake.

  1. Remove unused columns and filter rows at the source.

  2. Drop all-null/all-zero fact rows that never contribute to results.

  3. Move low-cardinality string attributes off the fact table into dimensions with integer keys.

  4. Partition on high-filter columns (DateKey, TenantKey) so the engine skips entire files. Use Z-order clustering when partitioning creates too many small files.

  5. Presort on the most filtered/grouped column first (e.g., DateKey, then ProductKey). RLE compression improves dramatically when values cluster into longer runs per segment.

  6. Use optimal data types. See MDL003.


MDL001: Many-to-Many Relationship Optimization

Bridge tables create expanded tables the engine materializes every query. The right layout depends on filter paths, bridge cardinality, and RLS. Test each option. Scenario: User (security), Customer (dimension), UserCustomer (bridge), Fact.

A — Canonical (bidir bridge): User 1──* UserCustomer *──bidir──1 Customer 1──* Fact Customer filters Fact directly; bridge only traversed for User. Best when User is rarely a slicer alongside Customer. Bidir causes high FE cost when both filter together.

B — M2M bridge to fact (no bidir):

User 1──* UserCustomer *──1 Customer
                │
                *──M2M──* Fact

Both dims always filter through bridge M2M. Best when consistent query times matter more than peak Customer-only performance.

C — Optimized hybrid: User 1──* UserCustomer *──M2M──* Fact *──1 Customer Customer filters Fact directly; User filters through bridge M2M. No bidir. Best general-purpose layout. Use inactive relationship + USERELATIONSHIP if you need Customer↔UserCustomer cross-queries.

D — Pre-computed combination key: User 1──* UserCombinations *──M2M──* Fact *──1 Customer ETL assigns a surrogate key per unique set of customers a user can access — users with identical access share one key. Best when bridge is very large or many users share the same access patterns.


MDL002: Star Schema Conformance

Snowflake schemas force multiple SE joins per query. Flatten dimension chains into a single wide dimension to reduce join depth and enable better fusion.

Sales ──* Product ──* Subcategory ──* Category → Sales ──* Product [ProductKey, ProductName, Subcategory, Category]


MDL003: Column Cardinality and Data Type Optimization

High-cardinality columns inflate dictionary size and segment memory.

  • Integer keys over string keys: Replace "PROD-001234" with integer surrogates.

  • Reduce timestamp precision: DateTime → Date when queries only group by date.

  • Bin continuous values: 50K distinct decimals → binned ranges if measure logic allows.

  • Split high-cardinality columns: FullAddress (100K distinct) → City, State, Zip.


MDL004: Aggregation Table Strategies

Pre-summarized Import tables intercept SE queries before they hit large DQ facts. Aggregate Awareness redirects automatically — no DAX changes.

Setup: GROUP BY [FKs], SUM([Metrics]) → load as Import → connect to same dimensions → map in Manage Aggregations as SUM OF [FactTable[Column]]. Fact tables must be DQ.

Filtered Aggs (hot/cold split): Import only recent data (e.g., last 3 months). 95%+ queries served from Import.


MDL005: Pre-Compute Period Comparison Columns

Period-over-period calcs (YoY, MoM) require two SE scans. Pre-computing prior-period values as physical columns on the fact row reduces it to one scan.

Before (two scans):

YoY = SUM ( 'Fact'[Sales] ) - CALCULATE ( SUM ( 'Fact'[Sales] ), SAMEPERIODLASTYEAR ( 'Date'[Date] ) )

After (one scan):

YoY = SUM ( 'Fact'[Sales] ) - SUM ( 'Fact'[SalesLY] )

Wider fact table, but eliminates the TI scan entirely. Best for fixed period comparisons on large DQ tables.


MDL006: Row-Based Time Intelligence Table

DAX TI functions break vertical fusion — each period measure gets its own SE query. A row-based TI table pre-materializes all periods as data rows so all period measures fuse into a single SE scan.

Table: Period (slicer label), Date (actual dates → relationship to fact), AxisDate (x-axis anchor). Relate via M2M to Fact or BiDir through Calendar.


MDL007: Eliminate Referential Integrity Violations

Fact FKs with no matching dimension row prevent inner-join rewriting for SWITCH/multi-measure patterns.

Detection:

SELECT [Dimension_Name], [RIVIOLATION_COUNT]
FROM $SYSTEM.DISCOVER_STORAGE_TABLES
WHERE [RIVIOLATION_COUNT] > 0

Fix: Add an "Unknown" catch-all row to the dimension and map missing foreign keys in fact to "Unknown" record.


MDL008: Replace SEARCH/FIND Filters with Pre-Computed Boolean Columns

SEARCH()/FIND() in filters forces row-by-row string scanning. Pre-compute the result as a boolean column (cardinality 2, ~1 bit/row) for pure columnar access. Generalizes to any fixed-value logical test — date flags, category indicators, prefix checks.


MDL009: Cardinality Reduction via Historical Value Substitution

Replace old key values beyond a retention window with a single placeholder to collapse cardinality and shrink dictionaries. This can be done in both facts and dimensions.

CASE WHEN SaleDate >= DATEADD(year, -1, GETDATE()) THEN SalesKey ELSE 'Historical Key' END

Section 6: Tier 4 Direct Lake Optimization Patterns

STOP — Requires user approval before applying any change. Changes here require Spark/ETL jobs or Fabric resource profile configuration outside the semantic model. Coordinate with the user's data engineering workflow.

Direct Lake reads OneLake Delta/Parquet files and loads columns into VertiPaq on demand. Its speed depends on source file layout, memory residency, and segment health.

DL001: V-Ordering Delta Tables for Direct Lake

Import refresh builds optimized VertiPaq storage. Direct Lake depends on Delta/Parquet layout at query time, so use V-Order for read-heavy Power BI tables to improve compression and column loading.

Two approaches:

  • Spark: spark.conf.set("spark.sql.parquet.vorder.default", "true"), then run OPTIMIZE for existing Delta tables.

  • Fabric resource profile: Use the readHeavyForPBI resource profile, which enables V-Order-oriented write settings for Power BI reads.


DL002: Segment Size and Parallelism

Parquet row groups shape VertiPaq column segment size/count (see SE Parallelism Factor in Section 1).

Target: 1–16M rows per rowgroup. Too few rowgroups → single-threaded scans; too many tiny rowgroups → merge overhead. For small tables (< 1M rows) this rarely matters. Run OPTIMIZE regularly to consolidate small files into properly sized rowgroups.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an MCP server

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Microsoft
Publisher
Microsoft (Official)
Source
microsoft/skills-for-fabric · skills/semantic-model-authoring · commit f8fac11
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced
Also distributed in
microsoft/skills-for-fabric, microsoft/skills-for-fabric

AutoGineer is not affiliated with or endorsed by Microsoft.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download semantic-model-authoring.zip