---
name: design-pbi-report
description: Design and publish Power BI reports via the Fabric MCP. Creates PBIR report definitions with proper visual query projections, bookmarks, navigation, and professional layout. Connects to published semantic models and produces renderable reports in the PBI Service.
argument-hint: "[environment-name] [model-id-or-name] [optional: --report-type dashboard|analytical|operational] [optional: --pages N]"
---

# Design & Publish Power BI Report

Create professional Power BI reports via the Fabric MCP tools. Reports are built as PBIR (Power BI Report) definitions with proper schema versions, visual query projections, bookmarks, and page navigation — all published directly to the Fabric workspace.

Reports are designed for consulting-grade presentation — executive audiences at billion-dollar clients. Every design decision must be intentional, data-driven, and grounded in deep understanding of the semantic model.

## Arguments

- `$ARGUMENTS[0]` — Fabric environment name (from orchestrator, e.g., `Fabric-MCP-Sandbox`)
- `$ARGUMENTS[1]` — Semantic model ID or name to connect the report to
- `--report-type TYPE` — Report template: `dashboard` (KPI cards + summary charts), `analytical` (deep-dive with slicers + drill-through), `operational` (detail tables + status tracking). Default: `dashboard`
- `--pages N` — Number of pages to create. Default: 2
- `--name NAME` — Report display name

## Pre-Flight

Before designing a Power BI report:
- Confirm the target Fabric workspace and semantic model name
- Discover the semantic model: `fabric_list_semantic_models`, then `fabric_get_semantic_model` for connection details
- Enumerate tables, columns, measures, and relationships in the model before designing visuals
- Confirm the report's purpose and target audience with the user — executive dashboard vs operational detail vs analytical deep-dive
- Verify the Fabric MCP is connected (`/mcp` should show fabric-mcp)

## Guardrails

- **Never reference columns or measures that don't exist in the semantic model.** Always discover the model schema first. Incorrect column references cause blank visuals with no error message.
- **Respect relationship topology.** Cross-filtering direction and relationship cardinality affect measure calculations. Verify relationships before writing DAX or building visuals that span multiple tables.
- **Use the PBIR format exclusively.** Legacy PBIX cannot be created or modified programmatically. All reports are PBIR (Power BI Report — JSON-based definition).
- **Test measure expressions** with `fabric_execute_dax_query` before embedding them in visuals.

## Prerequisites

- Semantic model must be published and refreshed (data loaded) in the target workspace
- User must have `user_aad_object_id` for workspace access validation
- Use `fabric_list_environments` to verify workspace access

---

## Workflow

### Phase 1: Deep Model Discovery (CRITICAL — spend the most time here)

Model understanding drives everything. A surface-level schema scan (listing tables, columns, measures) is INSUFFICIENT. You must understand how the model works — relationships, filter propagation, measure logic, data quality — before designing a single visual.

#### Step 1.1: Relationship Topology (THE FOUNDATION)

```
fabric_get_model_schema(environment_name, model_id)
```

For EVERY relationship in the model, document:
- **Tables & columns**: Which tables connect via which columns?
- **Cardinality**: One-to-many, many-to-one, many-to-many?
- **Cross-filter direction**: Single (dimension → fact) or Both (bidirectional)?
- **Active or inactive?** If inactive — WHY? (Usually to avoid ambiguous paths. Inactive relationships get activated via `USERELATIONSHIP()` in specific measures.)
- **Filter propagation path**: If I filter Table A, which downstream tables does that filter reach, and through what chain?

**Draw the star schema topology:**
- What is the hub (central dimension)?
- What are the fact tables (transaction data)?
- Where are snowflake extensions (dimension → sub-dimension)?
- Where are bridge tables (many-to-many resolvers)?
- Where are the dead ends (leaf tables that receive but don't propagate filters)?

**Identify dangerous patterns:**
- **Bridge tables** create many-to-many fan-out. If one fact row links to N rows in the bridge, additive measures (SUM, COUNT) get multiplied N times when the bridge dimension is on a visual axis.
- **Reversed one-to-many** (dimension pushes INTO a bridge) — unusual and often signals fan-out risk.
- **Missing relationships** — if a dimension has no relationship to the fact table, it can't filter data.

#### Step 1.2: Dimension Safety Classification

Based on the relationship topology, classify EVERY dimension that might appear in the report:

| Classification | Criteria | Usage |
|---------------|----------|-------|
| **Axis-safe** | Direct FK to fact table (one-to-many, single direction). No bridge table in path. | Can be used as bar chart category, matrix row, any visual axis |
| **Slicer-only** | Reaches fact table through a bridge table (many-to-many path). Additive measures will over-count on axis. | Dropdown slicer or page filter ONLY. Never as visual axis with $ measures. |
| **Useless** | Sparse data (>50% null), only 1 value, or no relationship to fact table. | Remove from report entirely — it misleads users. |

**Validate with DAX queries:**
```
// Test for fan-out: sum a measure across all values of a dimension
EVALUATE
SUMMARIZECOLUMNS(
    'DimensionTable'[Column],
    "Total", [SomeMeasure]
)

// Compare the sum of the above to the unfiltered total
// If sum-of-parts >> total, the dimension has fan-out (over-counting)
```

Over-count ratios:
- **1.0x** = safe (no fan-out)
- **1.0-1.05x** = acceptable (minor rounding from multi-parent relationships)
- **>1.1x** = DANGEROUS — do not use as visual axis with additive measures

#### Step 1.3: Measure Analysis (in context of relationships)

For EVERY measure in the model:

1. **Read the DAX expression** — use `fabric_get_model_schema` to get it
2. **Explain in plain English** what it calculates
3. **Identify relationship dependencies:**
   - Does it use `USERELATIONSHIP()`? (Activates an inactive relationship)
   - Does it use `CALCULATE()` with table filters? (Overrides filter context)
   - Does it use `ALL()` / `ALLSELECTED()` / `ALLEXCEPT()`? (Removes/modifies filters)
   - Does it use `FILTER()` on a specific table? (Adds row-level filtering)
4. **Map filter context requirements:**
   - What MUST be filtered for this measure to return meaningful results?
   - What happens with NO filter context? (Does it return NULL? An absurdly large number? The correct grand total?)
   - What happens with a date filter? A dimension filter?
5. **Identify broken measures:**
   - `ALLSELECTED` on the wrong column (% measures that always return 100%)
   - Measures that reference columns that don't exist
   - Placeholder measures with empty expressions

**Run validation DAX queries:**
```
// Test measure with no filter
EVALUATE ROW("Value", [MeasureName])

// Test with date filter
EVALUATE CALCULATETABLE(
    ROW("Value", [MeasureName]),
    'Date Table'[Year] = 2026
)

// Test on a dimension axis to check for over-count
EVALUATE
SUMMARIZECOLUMNS(
    'DimensionTable'[Column],
    "Value", [MeasureName]
)
```

#### Step 1.4: Data Quality Assessment

Check for issues that affect report design:
- **Null foreign keys**: What % of fact table rows have NULL dimension FKs? (If >10%, dimension slicers/filters will lose data)
- **Sparse dimensions**: How many dimension values actually appear in the data? (A 12-value lookup where 11 are unused is worthless as a slicer)
- **Date coverage**: What date range has actual data? (Don't design a chart for 2020-2050 if data only exists for 2025-2026)

#### Step 1.5: Create Missing Measures

If the model doesn't have measures you need, CREATE THEM using `fabric_add_measure`:
- **Count measures**: `COUNTROWS()`, `DISTINCTCOUNT()` for KPI cards
- **Combined measures**: `[Measure A] + [Measure B]` for combo chart overlays
- **Fixed % measures**: `DIVIDE([Measure], CALCULATE([Measure], ALL('Table'[Column])))` to replace broken ALLSELECTED patterns
- **Time intelligence**: YoY change, running totals, period comparisons

Always put new measures in the existing measure table (e.g., `Calculated Key Measures`).

### Phase 2: Page Filter Architecture

Before designing visuals, define the filter requirements for each page:

| Page | Required Filters | Why |
|------|-----------------|-----|
| Overview | Date (Year) | Measures like Outstanding Principal return NULL without date context |
| Time Series | Date on X-axis provides context | No page-level date filter needed |
| Detail Drill | Date + primary dimension | Filtered view of specific subset |

**Rules:**
- If ANY visual on a page uses a measure that requires date context → page MUST have a date slicer
- If a page shows breakdown by a dimension → add that dimension as a slicer or use it as a visual axis
- Slicer-only dimensions (bridge table paths) → dropdown slicer, NEVER visual axis

### Phase 3: Design Report Layout

#### Design System

**Color Palette (Consulting-Grade):**
- Primary: `#1B3A5C` (dark navy — headers, table headers, emphasis)
- Secondary: `#2E6B9E` (blue — primary chart series)
- Accent: `#C4782A` (warm amber — headline KPI accent borders, alerts)
- Canvas: `#F4F5F7` (warm light gray, applied via background shape)
- Cards: `#FFFFFF` with 1px `#D1D5DB` border
- Data sequence (8 colors, colorblind-safe): `#2E6B9E`, `#C4782A`, `#3A8F6E`, `#7B5EA7`, `#D4554A`, `#2B8C96`, `#D4A030`, `#6AADA8`

**Typography (Segoe UI throughout):**

| Element | Size | Weight | Color |
|---------|------|--------|-------|
| Report title | 20pt | Bold | `#FFFFFF` (on header bg) or `#1B3A5C` (on light bg) |
| KPI card value | 28pt | Bold | `#1B3A5C` |
| KPI card label | 9pt | Regular | `#5A6B7F` |
| Chart title | 10pt | Semibold | `#1A1A2E` |
| Axis/data labels | 8pt | Regular | `#5A6B7F` |
| Table header | 9pt | Semibold | `#FFFFFF` on `#1B3A5C` background |
| Table cell | 9pt | Regular | `#1A1A2E` |
| Footer text | 8pt | Regular | `#5A6B7F` |

**Grid System (1280x720 canvas, 16px base unit):**
- Edge margins: 16px left/right
- Usable width: 1248px
- Gutter between visuals: 16px
- 4-column: 300px each at x: 16, 332, 648, 964
- 2-column: 616px each at x: 16, 648
- Full-width: 1248px at x: 16

**Page Template (consistent across all pages):**

| Zone | Y range | Content |
|------|---------|---------|
| Background | Full page | Shape 1280x719, z:0, fill color with ~81% transparency |
| Title area | y:10-50 | Textbox (report name), nav buttons, reset button |
| Slicer bar | y:64-132 | 3 dropdown slicers, 58px height each |
| KPI cards | y:148-244 | 4 cards, 300x96 each, 4-column layout |
| Content row 1 | y:260-480 | Primary charts (full-width or 2-column) |
| Content row 2 | y:500-672 | Secondary charts + detail tables |
| Footer | y:692-716 | "Data as of" textbox + page label |

#### Visual Selection Guide

| Data Question | Visual Type | Axis/Category | Value |
|--------------|------------|--------------|-------|
| Single headline number | `cardVisual` | N/A | One measure |
| Ranked comparison (which entity has most?) | `barChart` (horizontal) | Axis-safe dimension | Measure, sorted desc |
| Trend over time | `lineStackedColumnComboChart` | Date column (Year, Month) | Measures on Y and Y2 |
| Composition breakdown | `treemap` | Axis-safe dimension (<15 cats) | Measure |
| Detail drill-down | `tableEx` or `pivotTable` | Multiple columns + measures | TopN or filtered |
| User-controlled filter | `slicer` | Any dimension | Dropdown mode (58px+) |
| Part of total (avoid if possible) | `donutChart` | <7 categories ONLY | Measure |

**Data label rules:**
- Bar charts: ON (display units: millions, 1 decimal, position: outside end, color: `#5A6B7F`)
- Treemaps: ON (category name + value, color: `#FFFFFF`, 8pt bold)
- Combo/column/line charts: OFF (too many data points — axes are sufficient)
- Tables: N/A (values in cells)

### Phase 4: Build PBIR Definition

Build ALL parts in a single definition — do NOT use incremental `fabric_add_visual` / `fabric_add_report_page`. Create the complete report in one `fabric_create_report` call with `definition_parts`.

For large definitions (>50KB), write a Node.js deployment script that reads the minified JSON file and calls the MCP endpoint via HTTP (Streamable HTTP transport). The `definition_parts` parameter cannot reliably handle inline content at that size.

### Phase 5: Deploy & Iterate

1. Deploy via `fabric_create_report` with `definition_parts`
2. User reviews in PBI Service
3. Collect feedback: spacing, visual choices, data accuracy, colors, navigation
4. Fix issues and redeploy (use `fabric_create_report` with new display name for each version, or `fabric_update_report_definition` on existing report)
5. Repeat until approved

---

## PBIR Technical Reference

### Schema Versions (Confirmed Working — 2026-03-18)

| File | Schema Version |
|------|---------------|
| `definition.pbir` | `definitionProperties/2.0.0` |
| `definition/version.json` | `versionMetadata/1.0.0`, version `"2.0.0"` |
| `definition/report.json` | `report/3.2.0` |
| `definition/pages/pages.json` | `pagesMetadata/1.0.0` |
| `definition/pages/{id}/page.json` | `page/2.1.0` |
| `definition/pages/{id}/visuals/{id}/visual.json` | `visualContainer/2.7.0` |
| `definition/bookmarks/bookmarks.json` | `bookmarksMetadata/1.0.0` |
| `definition/bookmarks/{id}.bookmark.json` | `bookmark/2.1.0` |
| `.platform` | `platformProperties/2.0.0` |

### Required Parts (Minimum)

```
definition.pbir                              — model connection reference
.platform                                    — metadata (type: "Report")
definition/version.json                      — PBIR version
definition/report.json                       — theme, settings
definition/pages/pages.json                  — page order + active page
definition/pages/{pageId}/page.json          — per-page config
definition/pages/{pageId}/visuals/{vizId}/visual.json  — per-visual config
```

### Optional Parts

```
definition/bookmarks/bookmarks.json                    — bookmark index
definition/bookmarks/{bookmarkId}.bookmark.json        — bookmark definition
StaticResources/SharedResources/BaseThemes/{name}.json — custom theme
```

### definition.pbir

```json
{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definitionProperties/2.0.0/schema.json",
  "version": "4.0",
  "datasetReference": {
    "byConnection": {
      "connectionString": "semanticmodelid={MODEL_GUID}"
    }
  }
}
```

### .platform

```json
{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/gitIntegration/platformProperties/2.0.0/schema.json",
  "metadata": { "type": "Report", "displayName": "Report Name" },
  "config": { "version": "2.0", "logicalId": "00000000-0000-0000-0000-000000000000" }
}
```

### definition/report.json

```json
{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/report/3.2.0/schema.json",
  "themeCollection": {
    "baseTheme": {
      "name": "CY24SU06",
      "reportVersionAtImport": { "visual": "2.6.0", "report": "3.1.0", "page": "2.3.0" },
      "type": "SharedResources"
    }
  },
  "settings": {
    "useStylableVisualContainerHeader": true,
    "exportDataMode": "AllowSummarized",
    "defaultDrillFilterOtherVisuals": true,
    "allowChangeFilterTypes": true,
    "useEnhancedTooltips": true
  }
}
```

**WARNING**: Do NOT include `layoutOptimization` — the `report/3.2.0` schema rejects it at import time.

### pages.json

```json
{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/pagesMetadata/1.0.0/schema.json",
  "pageOrder": ["page-id-1", "page-id-2"],
  "activePageName": "page-id-1"
}
```

### Page Definition

```json
{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/page/2.1.0/schema.json",
  "name": "{pageId}",
  "displayName": "Page Title",
  "displayOption": "FitToPage",
  "height": 720,
  "width": 1280
}
```

Hidden page: add `"visibility": "HiddenInViewMode"`

### Visual Query Projection Format

**Measure reference:**
```json
{
  "field": {
    "Measure": {
      "Expression": { "SourceRef": { "Entity": "MeasureTableName" } },
      "Property": "MeasureName"
    }
  },
  "queryRef": "MeasureTableName.MeasureName",
  "nativeQueryRef": "MeasureName"
}
```

**Column reference:**
```json
{
  "field": {
    "Column": {
      "Expression": { "SourceRef": { "Entity": "TableName" } },
      "Property": "ColumnName"
    }
  },
  "queryRef": "TableName.ColumnName",
  "nativeQueryRef": "ColumnName",
  "active": true
}
```

**CRITICAL**: Entity names in `SourceRef` must EXACTLY match the table names from `fabric_get_model_schema`. Common mistake: using "Dim - X" when the model uses "Lookup - X".

### Visual Types & Query State Roles

| Visual Type | queryState Roles | Description |
|-------------|-----------------|-------------|
| `cardVisual` | `Data.projections[]` | KPI cards — measures only |
| `barChart` | `Category.projections[]`, `Y.projections[]` | Horizontal bar chart |
| `clusteredBarChart` | `Category.projections[]`, `Y.projections[]` | Clustered horizontal bars |
| `columnChart` | `Category.projections[]`, `Y.projections[]` | Vertical column chart |
| `clusteredColumnChart` | `Category.projections[]`, `Y.projections[]` | Clustered vertical columns |
| `lineChart` | `Category.projections[]`, `Y.projections[]` | Trend lines |
| `lineStackedColumnComboChart` | `Category.projections[]`, `Y.projections[]`, `Y2.projections[]` | Combo — columns + line on two Y axes |
| `treemap` | `Group.projections[]`, `Values.projections[]` | Hierarchical area chart |
| `donutChart` | `Category.projections[]`, `Y.projections[]` | Part-to-whole (use sparingly) |
| `tableEx` | `Values.projections[]` | Flat table — columns + measures |
| `pivotTable` | `Rows.projections[]`, `Values.projections[]` | Matrix with expandable rows |
| `slicer` | `Values.projections[]` | Filter control |
| `textbox` | N/A (uses `objects.general.paragraphs`) | Static text |
| `actionButton` | N/A (uses `visualContainerObjects.visualLink`) | Navigation button |
| `shape` | N/A (uses `objects.fill`, `objects.line`, `objects.rotation`) | Background rectangle |

### Visual Examples

#### Full-Page Background Shape
```json
{
  "name": "bg-shape",
  "position": { "x": 0, "y": 0, "z": 0, "width": 1280, "height": 719, "tabOrder": 0 },
  "visual": {
    "visualType": "shape",
    "objects": {
      "line": [{ "properties": { "show": { "expr": { "Literal": { "Value": "false" } } } } }],
      "fill": [{ "properties": {
        "fillColor": { "solid": { "color": "#1B3A5C" } },
        "transparency": { "expr": { "Literal": { "Value": "81D" } } }
      } }],
      "rotation": [{ "properties": { "angle": { "expr": { "Literal": { "Value": "0L" } } } } }]
    },
    "drillFilterOtherVisuals": true
  }
}
```

#### Card Visual (KPI)
```json
{
  "name": "kpi-card",
  "position": { "x": 16, "y": 148, "z": 1, "width": 300, "height": 96, "tabOrder": 100 },
  "visual": {
    "visualType": "cardVisual",
    "query": {
      "queryState": {
        "Data": {
          "projections": [{
            "field": { "Measure": { "Expression": { "SourceRef": { "Entity": "Calculated Key Measures" } }, "Property": "Outstanding Principal" } },
            "queryRef": "Calculated Key Measures.Outstanding Principal",
            "nativeQueryRef": "Outstanding Principal"
          }]
        }
      }
    },
    "drillFilterOtherVisuals": true
  },
  "visualContainerObjects": {
    "background": [{ "properties": { "transparency": { "expr": { "Literal": { "Value": "0D" } } } } }]
  }
}
```

#### Card Accent Strip (4px colored left border)
```json
{
  "name": "kpi-accent",
  "position": { "x": 16, "y": 148, "z": 2, "width": 4, "height": 96, "tabOrder": 101 },
  "visual": {
    "visualType": "shape",
    "objects": {
      "line": [{ "properties": { "show": { "expr": { "Literal": { "Value": "false" } } } } }],
      "fill": [{ "properties": { "fillColor": { "solid": { "color": "#C4782A" } } } }],
      "rotation": [{ "properties": { "angle": { "expr": { "Literal": { "Value": "0L" } } } } }]
    },
    "drillFilterOtherVisuals": true
  }
}
```

#### Slicer (Dropdown)
```json
{
  "name": "slicer-year",
  "position": { "x": 16, "y": 69, "z": 1, "width": 240, "height": 58, "tabOrder": 200 },
  "visual": {
    "visualType": "slicer",
    "query": {
      "queryState": {
        "Values": {
          "projections": [{
            "field": { "Column": { "Expression": { "SourceRef": { "Entity": "Date Table" } }, "Property": "Year" } },
            "queryRef": "Date Table.Year",
            "nativeQueryRef": "Year",
            "active": true
          }]
        }
      }
    },
    "objects": {
      "data": [{ "properties": { "mode": { "expr": { "Literal": { "Value": "'Dropdown'" } } } } }],
      "general": [{ "properties": { "selfFilterEnabled": { "expr": { "Literal": { "Value": "true" } } } } }],
      "selection": [{ "properties": {
        "singleSelect": { "expr": { "Literal": { "Value": "false" } } },
        "selectAllCheckboxEnabled": { "expr": { "Literal": { "Value": "true" } } }
      } }]
    },
    "drillFilterOtherVisuals": true
  },
  "visualContainerObjects": {
    "background": [{ "properties": { "transparency": { "expr": { "Literal": { "Value": "100D" } } } } }],
    "visualHeader": [{ "properties": { "show": { "expr": { "Literal": { "Value": "false" } } } } }]
  }
}
```

#### Textbox (Transparent Background)
```json
{
  "name": "title-text",
  "position": { "x": 16, "y": 12, "z": 1, "width": 400, "height": 40, "tabOrder": 50 },
  "visual": {
    "visualType": "textbox",
    "objects": {
      "general": [{
        "properties": {
          "paragraphs": [{
            "textRuns": [{ "value": "Report Title", "textStyle": { "fontSize": "20pt", "fontWeight": "bold", "color": "#FFFFFF" } }]
          }]
        }
      }]
    },
    "drillFilterOtherVisuals": true
  },
  "visualContainerObjects": {
    "background": [{ "properties": { "transparency": { "expr": { "Literal": { "Value": "100D" } } } } }]
  }
}
```

#### Action Button (Navigation)
```json
{
  "name": "nav-overview",
  "position": { "x": 900, "y": 14, "z": 2, "width": 100, "height": 36, "tabOrder": 60 },
  "visual": {
    "visualType": "actionButton",
    "objects": {
      "icon": [{ "properties": { "shapeType": { "expr": { "Literal": { "Value": "'noShape'" } } } }, "selector": { "id": "default" } }],
      "text": [{ "properties": {
        "show": { "expr": { "Literal": { "Value": "true" } } },
        "text": { "expr": { "Literal": { "Value": "'Overview'" } } },
        "fontColor": { "solid": { "color": "#FFFFFF" } },
        "fontSize": { "expr": { "Literal": { "Value": "10D" } } },
        "fontFamily": { "expr": { "Literal": { "Value": "'Segoe UI'" } } },
        "bold": { "expr": { "Literal": { "Value": "true" } } },
        "alignment": { "expr": { "Literal": { "Value": "'Center'" } } }
      } }],
      "outline": [{ "properties": { "show": { "expr": { "Literal": { "Value": "false" } } } } }],
      "fill": [{ "properties": {
        "show": { "expr": { "Literal": { "Value": "true" } } },
        "fillColor": { "solid": { "color": "#2E6B9E" } }
      } }]
    },
    "visualContainerObjects": {
      "background": [{ "properties": { "transparency": { "expr": { "Literal": { "Value": "100D" } } } } }],
      "visualLink": [{
        "properties": {
          "show": { "expr": { "Literal": { "Value": "true" } } },
          "type": { "expr": { "Literal": { "Value": "'Bookmark'" } } },
          "bookmark": { "expr": { "Literal": { "Value": "'bm-overview'" } } },
          "tooltip": { "expr": { "Literal": { "Value": "'Navigate to Overview'" } } }
        }
      }]
    },
    "drillFilterOtherVisuals": true
  },
  "howCreated": "InsertVisualButton"
}
```

Inactive button variant: `fill.show: false`, `text.fontColor: "#B0C4D8"`, `text.bold` omitted.

#### Bookmarks

**bookmarks.json:**
```json
{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/bookmarksMetadata/1.0.0/schema.json",
  "items": [
    { "name": "bm-overview" },
    { "name": "bm-detail" },
    { "name": "bm-reset" }
  ]
}
```

**WARNING**: Use `items` array (not `bookmarkOrder`). The schema rejects unknown properties.

**Navigation bookmark:**
```json
{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/bookmark/2.1.0/schema.json",
  "displayName": "Overview",
  "name": "bm-overview",
  "options": { "targetVisualNames": [] },
  "explorationState": {
    "version": "1.3",
    "activeSection": "page-overview",
    "sections": { "page-overview": { "visualContainers": {} } },
    "objects": {}
  }
}
```

**Reset bookmark** (must include `activeSection` and `sections`):
```json
{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/bookmark/2.1.0/schema.json",
  "displayName": "Reset",
  "name": "bm-reset",
  "options": { "targetVisualNames": [] },
  "explorationState": {
    "version": "1.3",
    "activeSection": "page-overview",
    "sections": { "page-overview": { "visualContainers": {} } },
    "objects": {}
  }
}
```

---

## Deployment Gotchas (Verified 2026-03-18)

1. **`layoutOptimization` rejected** — `report/3.2.0` schema does not allow this property. Omit it entirely.

2. **`selector.data` must be an array** — For `dataViewWildcard` selectors:
   ```json
   "selector": { "data": [{ "dataViewWildcard": { "matchingOption": 1 } }] }
   ```
   NOT `"data": { "dataViewWildcard": ... }` (object rejected, array required).

3. **Bookmarks use `items` not `bookmarkOrder`** — `bookmarksMetadata/1.0.0` requires `items` array.

4. **Reset bookmark needs `activeSection` + `sections`** — The explorationState must include these even for a reset/clear-filters bookmark.

5. **Large definitions (>50KB) need deployment scripts** — Write a Node.js script that reads the minified JSON and calls the MCP endpoint via Streamable HTTP transport. The inline `definition_parts` parameter cannot reliably handle content this large.

6. **Card accent borders use shape visuals** — `visualContainerObjects.shape` is NOT a valid property on card visuals. Use a separate 4px-wide shape visual positioned at the card's left edge (z above the card).

7. **Every visual needs `drillFilterOtherVisuals: true`** — Without this, cross-filtering between visuals won't work.

8. **Textbox + button backgrounds default to opaque white** — Always set `visualContainerObjects.background.transparency: 100D` on textboxes and buttons, or the white background will obscure content on colored backgrounds.

9. **Slicer minimum height: 58px** — Below this, dropdown controls are too small to interact with. Include `selfFilterEnabled`, `selectAllCheckboxEnabled`, `singleSelect: false` in slicer objects.

---

## Common Measure Pitfalls

| Pattern | Problem | Solution |
|---------|---------|----------|
| Measure returns NULL | Requires filter context (e.g., date filter) that isn't present | Add required slicer to the page |
| % of Total = 100% for every row | `ALLSELECTED` targets wrong column — doesn't clear the visual axis filter | Create new measure with `DIVIDE([M], CALCULATE([M], ALL('Table'[AxisColumn])))` |
| Sum of parts >> total | Bridge table fan-out — one fact row counted N times through many-to-many | Move dimension to slicer (not axis), or use DISTINCTCOUNT instead of SUM |
| Cumulative measure gives same value at every point | `ALLSELECTED` vs `ALL` confusion in time-series filter | Use `FILTER(ALL('Date Table'), Date <= MIN(Date))` pattern for running totals |
| Measure ignores slicer | Measure uses `ALL()` on the filtered table, overriding slicer context | Check DAX — if it uses `ALL('Table')`, the slicer on that table is overridden by design |

---

## Report Creation Method

**ALWAYS build the complete PBIR definition in one call.** Do NOT use incremental `fabric_add_visual` / `fabric_add_report_page` for new reports — these use a get-modify-put cycle that can have LRO timing issues.

Instead, build all parts (pages, visuals, bookmarks) and pass them as `definition_parts` to `fabric_create_report`.

For modifications to existing reports, use `fabric_get_report_definition` → modify → `fabric_update_report_definition`.

## Validation

After creating or updating a report:
- Verify the report appears in the target workspace: `fabric_list_reports`
- Confirm page count and visual count match the design
- If possible, open the report in Power BI Service and verify visuals render with data
- Report: workspace, report name, page count, visual count, semantic model bound to
