# PBI Desk > One Power BI project export, reviewed five ways. Paste the semantic model definition (TMDL or > model.bim / TMSL JSON), the PBIP report layout, a Performance Analyzer capture, or all of them > concatenated, and get a semantic model design review, DAX measure rewrites, performance triage, > the corrected TMDL to paste back, and a report and accessibility review — over the same paste, > in one sitting. Live at https://pbi-desk.skillsafe.ai/ **PBI Desk is an independent tool. It is not affiliated with, endorsed by, or connected to Microsoft, Power BI or Microsoft Fabric. Power BI and Microsoft Fabric are trademarks of Microsoft Corporation.** ## What it is for A BI developer has a semantic model that grew, a report that is slow, and a stakeholder review on Thursday. The artifacts are all text: a `.pbip` project keeps the model as TMDL under `definition/`, the report as `report.json` (or per-page `visual.json` in the newer PBIR format), and Power BI Desktop exports a Performance Analyzer capture as JSON. PBI Desk takes those, reads them in the browser, and then runs whichever of five reviews the sitting needs. ## The free lane runs before anything is sent anywhere Everything below is computed in the visitor's browser, with no account and no charge. It runs over the **whole** paste, including any part the input clipper removes before a run, and its counts are passed into the run as `prescan_facts` so the reviewer cannot reason over a truncated file without knowing it. - **Input classification.** TMDL, `model.bim` / TMSL JSON, a PBIP `report.json`, a Performance Analyzer export, a pasted Performance Analyzer table, or a concatenation of several. Explicit `// ---- file: … ----` separators are honoured; without them the reader carves out the JSON documents itself with a string-aware bracket matcher, so a brace inside a quoted `config` string cannot end a span early. - **A real TMDL parser.** Indentation-aware, tabs or spaces. Tables, columns, calculated columns, measures in both the single-line `measure X = expr` form and the fenced multi-line form, hierarchies and their levels, partitions with storage mode, relationships with cardinality and cross-filter direction, and security roles with their filter expressions. `ref table` manifest lines are recognised as references, not definitions. - **A TMSL reader** producing the same object model, so a JSON export and a TMDL export are reviewed identically. - **Relationship graph analysis.** The active relationship graph is built and then interrogated: how many active filter paths run between each joined pair, where the cycles are, which relationships cross-filter in both directions, which are many-to-many, which join two fact tables directly, which dimension hangs off another dimension, which tables are disconnected, and which are reachable only through an inactive relationship. **More than one active path between two tables is the blocker this app exists to find** — it is the defect that silently makes two visuals disagree about the same total. - **A bracket-aware DAX lint** over every measure. String literals and comments are masked first, so a pattern cannot match inside them, and containment is decided by matching parentheses rather than by proximity — a whole-table `FILTER` is only reported when it really is inside a `CALCULATE`. It looks for: whole-table `FILTER` inside `CALCULATE`, nested iterators, an iterator over a whole-table `FILTER`, raw `/` division where `DIVIDE` is meant (ignoring a `/` inside a bracketed column name), `EARLIER`, `CALCULATE` with no filter arguments, `DISTINCTCOUNT` on a high-cardinality column, `ALL` where `REMOVEFILTERS` states the intent, `IF(ISBLANK(x), …)` double evaluation, hard-coded date literals, `RELATED` without a row context, a sub-expression repeated verbatim where one `VAR` would do, a long expression with no `VAR` at all, a string comparison in a filter predicate, context transition inside an iterator, and `IFERROR` used to hide rather than fix. - **Model hygiene.** Key columns still aggregating by default, measures with no format string, no display folder or no description, high-cardinality keys left visible, calculated columns, automatic date/time tables, a date table nothing marks as one, and mixed storage modes including a DirectQuery dimension feeding an Import fact. - **Performance Analyzer ranking.** Events are grouped per visual through the `parentId` chain into DAX, render and other milliseconds; "other" is the residual against the visual's lifecycle span rather than a sum of named events, so a slicer whose cost is neither query nor render is visible. Visuals are ranked by total time and measured against a per-visual budget the user sets. - **Report layout reading.** Pages, visual containers, visual types and field projections, including the `config` values that are themselves JSON strings. - **Exports, all free**: the prescan as Markdown, and CSV for tables, relationships, measures, findings, captured visuals and report visuals. ## The five lanes Every lane takes the same work object — one project export — and returns the same envelope with its own body. `app.js` always sends a `task` field naming the lane. | task | what it does | verdicts | | --- | --- | --- | | `model` | Semantic model design review: schema shape, table roles, storage modes, a verdict per relationship, and an ordered change plan | `sound`, `needs-rework`, `not-a-model` | | `perf` | Performance triage from the capture: bottlenecks ranked by total time with the cause categorised as dax / model / visual / refresh / gateway, against a budget | `within-budget`, `over-budget`, `no-capture` | | `dax` | Measure rewrites: every measure gets a row, including the ones left alone, with the original verbatim, the named anti-pattern, the expected gain and whether the number can change | `optimised`, `partially-optimised`, `no-measures-found` | | `tmdl` | Authoring: paste-ready definitions with a file path, what each replaces, ordered apply steps, and the lane's own validation of what it wrote | `ready-to-apply`, `needs-decision`, `not-authorable` | | `report` | Report review: per-page density verdicts, a per-visual decision that states the question the visual answers before it names the mark, layout notes and concrete accessibility checks | `ship-ready`, `needs-rework`, `no-report-layout` | Pipeline order in the app is Inspect (model, perf), Fix (dax, tmdl), Present (report). The handoff buttons follow it: a model review offers to rewrite the measures it flagged and to author the TMDL for its findings; a performance triage offers to take the DAX-dominant visual's measure into the DAX lane. ## The output contract One JSON object. The shared envelope is identical in every lane: `lane`, `title`, `dataset`, `verdict`, `verdict_reason`, `headline`, `input_kind`, `objects_seen {tables, measures, relationships, pages, visuals, captured_visuals}`, `findings[] {id, severity, area, object, what, why, fix, effort}`, `coverage_check[] {id, handling, note}`, `assumptions[] {assumption, risk_if_wrong}`, `open_questions[]`, `quick_wins[]`, `summary`. Per lane: - `model` → `schema {shape, shape_reason, fact_tables[], dimension_tables[], unclassified[]}`, `tables[]`, `relationships[]`, `model_actions[]` - `dax` → `measures[] {name, table, original, rewritten, pattern, why, expected_gain, behaviour_change, behaviour_note, verify}`, `dax_notes[]` - `perf` → `bottlenecks[]`, `budget {target_ms, worst_total_ms, over_budget_count, note}`, `next_captures[]` - `tmdl` → `artifacts[] {path, kind, name, tmdl, purpose, replaces}`, `apply_steps[]`, `validation[]` - `report` → `pages[]`, `visuals[]`, `layout_notes[]`, `accessibility {summary, checks[]}` ## The grounding contract Two rules make the review checkable rather than merely plausible. 1. **Every prescan flag comes back.** Each id in `prescan_facts.flags` must appear exactly once in `coverage_check`, either `addressed` or `set-aside` with a one-line reason. The page reconciles the reply against the reader afterwards and says so when a flag was neither. 2. **Nothing is invented.** Row counts, cardinalities and millisecond timings exist only where the input states them. In the performance lane every millisecond is copied from the capture; `expected_saving_ms` is the only figure reasoned to. In the DAX lane `original` is the expression as it appears in the input. The page also flags tables the reply names that are not in the paste, and count gaps between the reviewer and the reader. ## Programmatic use `https://api.skillsafe.ai/v1/app-api` — see https://pbi-desk.skillsafe.ai/api.html for the full tutorial in cURL, Python, JavaScript, Go, Java, Ruby, PHP and C#, and https://pbi-desk.skillsafe.ai/tokens.html for token management. Run body: `{task, project, context?, prescan_facts?, focus?, target_ms?}`. `estimate` is free and returns a per-lane `hold_credits` reservation, not a price; the settled charge is usually much lower. ## Privacy The parse, the relationship graph, the DAX lint, the Performance Analyzer ranking and every CSV on the page are computed in the browser. Nothing is uploaded by the reader. A run sends the clipped project text and the facts the reader computed — never a file. Reviews are stored in the signed-in user's own account records and mirrored in their browser. ## Sources Derived from five community skills in the `github/awesome-copilot` repository: - `@github/power-bi-model-design-review` - `@github/power-bi-dax-optimization` - `@github/power-bi-performance-troubleshooting` - `@github/powerbi-modeling` - `@github/power-bi-report-design-consultation` Published on SkillSafe (https://skillsafe.ai/). Model: `gpt-terra`.