microsoft/analyze-canvas-performance
Analyze and audit a Power Apps canvas app. USE WHEN the user wants to analyze, profile, audit, review, diagnose, or improve a Canvas App or pa.yaml files. USE FOR: slow apps, long load times, delegation warnings, N+1 database calls, excessive collections, ForAll optimization, OnStart overload, Named Formulas, Concurrent execution, Explicit Column Selection, DelayOutput on text inputs, cross-screen control references, Power Automate overuse, unused variables, nested galleries, error handling, App.OnError, form validation, naming conventions, variable scope misuse, modern controls, responsive layout, accessibility. DO NOT USE WHEN the app has not been synced locally yet — sync it first.
npx skills add https://github.com/microsoft/power-cat-skills --skill analyze-canvas-performance
Perform a full audit of the existing Canvas App and produce a prioritized findings report covering performance, coding standards, app design, and error handling:
$ARGUMENTS
Before analyzing anything, you MUST read and internalize the technical reference document:
${CLAUDE_PLUGIN_ROOT}/references/TechnicalGuide.md — Technical best practices, control selection, validation workflow, formulas, layout strategiesRead this file before planning any analysis.
Before reading any YAML files, call the sync_canvas MCP tool to ensure a local copy of the canvas app YAML is present and up to date.
Only proceed after sync_canvas completes successfully.
You MUST read .pa.yaml files for analysis purposes only. Never write, edit, create, or delete any .yaml or .pa.yaml file at any point during this skill execution. The YAML files are the source of truth for the app and must remain unchanged.
Before doing anything else, ask the user:
> What name should appear as the author of this report? (used in "Audited by", "Reviewed by", and "Generated by" fields)
Store the answer as {{AUTHOR}}. If the user provides no answer, use "Power Apps Team".
Run the following in parallel immediately after sync_canvas:
.pa.yaml file and build a structural inventory.App.pa.yaml is the app-level object file, not a screen. Never treat it as a screen or create a screen dependency entry for it. It is the source of App.OnStart, App.Formulas, App.OnError, and global app settings. Attribute findings from it to App (not a screen name).get_appchecker_errors — retrieves all formula errors, performance warnings, and data-source issues detected by the Power Apps App Checker engine.get_accessibility_errors — retrieves all accessibility issues detected by the Power Apps Accessibility Checker.Store the raw results from both tools — they will be processed in Section 6.
From the YAML files inventory:
App.OnStart, App.Formulas, and App.OnError contentSet()), context variables (UpdateContext()), and collections (ClearCollect()/Collect())Share a discovery summary:
> Discovery complete.
>
> | Screen | Control Count | Data Sources Referenced |
> |--------|--------------|------------------------|
> | [Name] | [N] | [list] |
>
> Total controls: [N] | Total screens: [N] | Data sources: [list] | Flow calls: [N]
> App Checker issues: [N] | Accessibility issues: [N]
For every issue found, record: Severity (🔴 High / 🟡 Medium / 🟢 Low), Location, Finding, and Recommendation.
ClearCollect / Collect / Set chains in App.OnStart that delay startup.Navigate() calls inside App.OnStart — replace with App.StartScreen (declarative, doesn't block rendering): // GOOD
App.StartScreen = If(Param("AdminMode") = "1", AdminScreen, HomeScreen)
Set() variables are read-only after initialization — if so, migrate to Named Formulas (App.Formulas): they are lazy, auto-updated, and don't execute at startup.OnStart that are only used on one screen — move them to that screen's OnVisible.OnVisible when the data doesn't change between visits — add If(IsEmpty(collection), ClearCollect(...)) guards.First(Filter(...)) or First(Search(...)) — replace with LookUp(...) (delegable, returns one record without loading the dataset).Left, Mid, Len, IsBlank, in operator, Search()) applied to large external tables — recommend delegable alternatives or server-side views.ClearCollect(col, DataSource) calls that retrieve all columns when only a subset is needed — recommend ShowColumns() + Filter(): // GOOD
ClearCollect(colAcc,
ShowColumns(Filter(Accounts, !IsBlank('Address 1: City')), "name", "address1_city")
)
Count(Filter(...)) patterns — replace with CountIf(DataSource, condition) for a single delegable call.ClearCollect / Collect calls in OnStart or OnVisible that each target an external data source (Dataverse, SharePoint, SQL, a connector, or a Power Automate flow) — these are HTTP calls that can run in parallel. Wrap them in Concurrent(...): // GOOD — each targets a different external source
Concurrent(
ClearCollect(colAccounts, Accounts),
ClearCollect(colUsers, Users),
ClearCollect(colSettings, 'Environment Variable Definitions')
);
ClearCollect(colFiltered, Filter(colLocal, ...))) — these are in-memory operations with no HTTP overhead; Concurrent() adds no benefit there.Concurrent() to independent calls — never when one call depends on another's output.LookUp(...), Filter(...), connector calls, or Power Automate flow calls inside gallery item properties (Label.Text, Image.Image, Icon.Color, etc.) only when the data source is external — i.e., a Dataverse table, a SharePoint list, a SQL table, an OData feed, or a named flow. Each gallery row triggers a separate HTTP call (N+1 pattern). // BAD — LookUp against an external Dataverse table; 1 HTTP call per row
Label.Text = LookUp(Users, UserID = ThisItem.CreatedBy).FullName
// GOOD — traverse the Dataverse relationship; resolved server-side
Label.Text = ThisItem.'Created By'.'Full Name'
LookUp(colLocalCollection, ...) or Filter(colLocalCollection, ...) — local collections live in memory and have zero HTTP cost.AddColumns to pre-join external data into a local collection before binding the gallery; Dataverse views or SQL views to flatten data server-side.CountRows(Filter(ExternalSource, ...)) patterns inside gallery item formulas — triggers a full table scan per row render; replace with CountIf(ExternalSource, condition).Office365Users.MyProfile(), MSN.GetCurrentWeather()) nested inside Filter(), LookUp(), or ForAll() — the call fires once per evaluation: // BAD — API called every time the filter evaluates
Filter(Product, Email = Office365Users.MyProfile().Mail)
// GOOD — call once, store result
Set(varUserEmail, Office365Users.MyProfile().Mail);
Filter(Product, Email = varUserEmail)
App.OnStart or via a Named Formula.ForAll — exponential iterations; recommend flattening data first.Patch() inside ForAll() — one server call per record. Invert the pattern for a single batched call: // BAD — N calls
ForAll(colData, Patch(DataSource, { field: "value" }));
// GOOD — 1 batched call
Patch(DataSource, ForAll(colData, { field: "value" }));
ForAll chains (multiple nested ForAll, AddColumns(ForAll(...)), or ForAll with multi-step Patch/Collect logic) where the data source is Dataverse — the business logic is running client-side over HTTP. Recommend moving the logic server-side:ForAll/AddColumns entirely.ForAll loops. // INSTEAD OF complex client-side ForAll:
// Call a Custom API that returns pre-joined data
Set(varResult, MyCustomAPI.GetOrderSummary({ customerId: gblUserId }))
Search() delegation, limited Filter operators).ShowColumns() + delegable Filter() to limit returned data.'Environment Variable Definitions' or LookUp('Environment Variable Values', ...) calls). // BAD — 3 separate HTTP calls
Set(varApiUrl, LookUp('Environment Variable Values', 'Environment Variable Definition'.schemaname = "cr123_ApiUrl").Value);
Set(varTenantId, LookUp('Environment Variable Values', 'Environment Variable Definition'.schemaname = "cr123_TenantId").Value);
Set(varFeatureFlag, LookUp('Environment Variable Values', 'Environment Variable Definition'.schemaname = "cr123_FeatureFlag").Value);
ClearCollect — retrieve all needed environment variable values in one query and look up from the local collection: // GOOD — 1 HTTP call, then local lookups
ClearCollect(colEnvVars, 'Environment Variable Values');
Set(varApiUrl, LookUp(colEnvVars, ...).Value);
Concurrent() — if the variables must each be in their own global variable, wrap all the LookUp / Set calls in Concurrent(): // GOOD — all 3 HTTP calls fire in parallel
Concurrent(
Set(varApiUrl, LookUp('Environment Variable Values', ...).Value),
Set(varTenantId, LookUp('Environment Variable Values', ...).Value),
Set(varFeatureFlag, LookUp('Environment Variable Values', ...).Value)
);
Set() calls that fetch different properties from the same API in separate calls — consolidate into one call and access properties: // BAD — 4 API calls
Set(varName, Office365Users.MyProfile().DisplayName);
Set(varCity, Office365Users.MyProfile().City);
// GOOD — 1 API call
Set(varEmployee, Office365Users.MyProfile())
Clear(); Collect(...) patterns — replace with the single ClearCollect(...) call.TextInput controls used as live search/filter boxes where DelayOutput is false (default) — every keystroke retriggers gallery filtering or data calls.DelayOutput = true to introduce a ~500ms debounce before the formula evaluates.Screen2.Label3.Text) — Power Apps loads Screen B into memory prematurely, increasing initial load time.Visible = false controls that are always rendered (non-component) — loading an invisible control still consumes memory; use conditional component rendering instead.Image controls bound to base64 strings stored in variables or collections — recommend storing URLs instead..xlsx file).in operator on a SQL/Dataverse column instead of StartsWith — in causes a table scan, StartsWith leverages an index.Filter(DataSource, condition) on columns that are likely unindexed (non-PK, non-FK, non-choice columns in Dataverse; text columns without a SQL index).Sort / SortByColumns calls on unindexed columns.Filter, LookUp, and Sort.in with StartsWith where prefix matching is acceptable.Button1, Label3, TextInput5) — apply the standard 3-character type prefix + camelCase:| Control | Prefix | Control | Prefix |
|---------|--------|---------|--------|
| Button | btn | Label | lbl |
| TextInput | txt | Gallery | gal |
| Form | frm | Image | img |
| Container | con | Icon | ico |
| Dropdown | drp | ComboBox | cmb |
gbl prefix and context variables without a loc prefix.col prefix.Home Screen, Search Screen).Set(...)) used for state that only matters on one screen — replace with context variables (UpdateContext(...)).If/Switch, multi-step ForAll, custom error handling) but have no comments — 🟢 Low.App.OnStart with more than 10 Set()/Collect() calls and no section marker comments. // Line comment — explain the next line
/* Block comment:
Use for multi-line explanations or section markers */
Items source contains another gallery or whose child controls modify the gallery's Items in OnChange / OnSelect — nested galleries cause performance issues and potential infinite loops.ComboBox, Slider, DatePicker, or Toggle controls inside galleries — their OnChange fires unexpectedly when data changes.Parent.Width, Parent.Height).Height = Parent.Height).Width, Height, BorderColor, and Fill properties; support nesting; and create a logical structure that screen readers and accessibility tools can interpret.Set() variables in App.OnStart that could be eliminated by adopting modern controls with an app theme.FormMode switching (FormMode.New / FormMode.Edit / FormMode.View).Patch(), SubmitForm(), Remove(), and connector calls with no IfError(...) wrapper and no user notification on failure. // GOOD
IfError(
Patch(Orders, Defaults(Orders), { Status: "New" }),
Notify("Save failed: " & FirstError.Message, NotificationType.Error)
)
SubmitForm(frm...) buttons where the form's OnFailure property is empty — users receive no feedback when submission fails.Recommend: OnFailure = Notify("Error: " & Form.Error, NotificationType.Error)
App.OnError is empty — unhandled errors show raw system messages to users and are not logged. // GOOD — centralized error logging
Notify(
Concatenate("Error: ", FirstError.Message,
" | Source: ", FirstError.Source),
NotificationType.Error
)
traces table with the message "Unhandled error", enabling proactive alerting without waiting for user reports. traces
| where message == "Unhandled error"
| extend d = parse_json(customDimensions)
| extend errors = parse_json(tostring(d.errors))
| mv-expand errors
| project timestamp, AppName = d['ms-appName'], ErrorMessage = errors.Message
| order by timestamp desc
⚠️ Works only with custom connectors — standard connectors are not supported.
Process the raw results collected in Step 1 from get_appchecker_errors and get_accessibility_errors.
For each item returned by get_appchecker_errors:
App Checker, Area = the checker rule name or category (e.g., Formula Error, Performance, Data Source, Deprecated), Location = screen + control path as returned by the tool, Code Found = the offending formula snippet (≤ 120 chars), Finding = the checker message verbatim, Recommendation = actionable fix.(also flagged in Section [N]) instead of a full duplicate row.get_appchecker_errors returns no issues, record a single 🟢 Low informational row: *"App Checker returned no errors or warnings. The app is clean per the checker."*For each item returned by get_accessibility_errors:
Accessibility, Area = the accessibility rule (e.g., Missing Label, Color Contrast, Tab Order, Screen Reader, Keyboard Navigation), Location = screen + control name, Code Found = the property value that caused the issue (e.g., the empty AccessibleLabel value), Finding = the checker message verbatim, Recommendation = actionable fix.get_accessibility_errors returns no issues, record a single 🟢 Low informational row: *"Accessibility Checker returned no errors. The app meets baseline accessibility standards."*Analyze all data access patterns in the app. Read .pa.yaml files only — do not modify them.
Read the OnStart property from App.pa.yaml and identify every query against an external data source (Dataverse, SharePoint, SQL, connector call, Power Automate flow). Classify each query by its execution context:
Concurrent(): the entire Concurrent block counts as 100 ms (all queries inside fire in parallel — the block's cost is the slowest single query, approximated as 100 ms).If() condition: the query may or may not run at runtime. Track separately as a conditional query.ForAll() loop: mark as N+1. Each distinct query call inside the loop costs 100 ms per iteration. Report for the best case (loop runs exactly once).Produce three KPI values to render in the HTML report:
| KPI | Calculation Rule |
|-----|-----------------|
| Best-Case Start Time | Sum 100 ms per sequential query and per Concurrent block that is not inside any If() branch (conditional queries assumed false — skipped). |
| Worst-Case Start Time | Sum 100 ms per sequential query and per Concurrent block including those inside If() branches (all branches assumed true). |
| ForAll N+1 Risk (1 iteration) | For each ForAll that contains external queries: sum 100 ms × number of distinct query calls inside, assuming the loop runs exactly once. Note in the report that actual runtime cost = this value × N records. |
For each screen .pa.yaml file (skip App.pa.yaml — it is not a screen):
Filter(), LookUp(), Search(), ClearCollect(), Collect() call against an external data source, plus any connector or Power Automate flow call.From App.pa.yaml → App.Formulas, identify every named formula that contains:
Filter, LookUp, Search, ClearCollect, Collect), orOffice365Users.*, MSN.*, any custom connector method), orFor each qualifying named formula:
.pa.yaml files for references to this formula name.For each screen, identify pairs or groups of queries where:
Status = "Active" vs Status = "Inactive").Flag 🔴 High for identical duplicate queries (pure redundant round-trips). Flag 🟡 Medium for near-duplicates (consolidation opportunity). Include the control.property and formula snippet for each query in the group.
Identify every query that retrieves records without restricting columns — no ShowColumns() wrapper and no ECS-enforced explicit column selection — from an external data source. For each:
App.OnStart / App.Formulas), control.property, exact query code (≤ 120 chars), data source name, estimated columns returned.schema not determinable from YAML — all columns returned.ShowColumns() still recommended).After completing all audits, determine the output filename:
Performance & Quality Review.html first..pa.yaml files, append a timestamp: Performance & Quality Review_YYYYMMDD_HHmmss.html (e.g. Performance & Quality Review_20260419_143022.html).Write the report using the template below, replacing all {{PLACEHOLDER}} tokens with real data.
Then post a brief in-chat confirmation:
> Report saved: [resolved filename]
> 🔴 High: [N] · 🟡 Medium: [N] · 🟢 Low: [N] · Total findings: [N]
> Open the file in a browser, then copy-paste the content into Outlook to share.
For the Code Found column extract the exact formula or property value from the .pa.yaml file (≤ 120 characters). Use <code> tags for inline formulas. For missing/empty properties write <em>(property empty)</em>. For structural issues write the count, e.g. <em>(62 controls)</em>.
For severity badges use:
<span style="background:#C50F1F;color:#fff;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">High</span><span style="background:#F7630C;color:#fff;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">Medium</span><span style="background:#107C10;color:#fff;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:600;">Low</span><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Canvas App Audit Report — {{APP_NAME}}</title>
</head>
<body style="margin:0;padding:0;background-color:#f3f2f1;font-family:'Segoe UI',Arial,sans-serif;font-size:14px;color:#323130;">
<table width="100%" cellpadding="0" cellspacing="0" style="background-color:#f3f2f1;">
<tr><td align="center" style="padding:32px 16px;">
<table width="800" cellpadding="0" cellspacing="0" style="max-width:800px;background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.12);">
<!-- HEADER -->
<tr>
<td style="background-color:#742774;padding:32px 40px;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td>
<p style="margin:0 0 4px 0;font-size:11px;color:#e6c9e6;letter-spacing:1px;text-transform:uppercase;">Power Apps · Canvas App</p>
<h1 style="margin:0 0 8px 0;font-size:26px;font-weight:700;color:#ffffff;">Performance & Quality Audit</h1>
<p style="margin:0;font-size:18px;color:#e6c9e6;font-weight:400;">{{APP_NAME}}</p>
</td>
<td align="right" valign="top">
<p style="margin:0;font-size:12px;color:#d4a8d4;">{{REPORT_DATE}}</p>
<p style="margin:4px 0 0 0;font-size:12px;color:#d4a8d4;">Audited by: {{AUTHOR}}</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- SCORE CARDS -->
<tr>
<td style="padding:28px 40px 0 40px;">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="padding-right:12px;">
<table width="100%" cellpadding="16" style="background:#fde7e9;border-radius:6px;border-left:4px solid #C50F1F;">
<tr><td>
<p style="margin:0;font-size:11px;color:#842029;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">High Severity</p>
<p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#C50F1F;">{{TOTAL_HIGH}}</p>
</td></tr>
</table>
</td>
<td style="padding-right:12px;">
<table width="100%" cellpadding="16" style="background:#fff4ce;border-radius:6px;border-left:4px solid #F7630C;">
<tr><td>
<p style="margin:0;font-size:11px;color:#7a4100;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Medium Severity</p>
<p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#F7630C;">{{TOTAL_MEDIUM}}</p>
</td></tr>
</table>
</td>
<td style="padding-right:12px;">
<table width="100%" cellpadding="16" style="background:#dff6dd;border-radius:6px;border-left:4px solid #107C10;">
<tr><td>
<p style="margin:0;font-size:11px;color:#0e4a0b;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Low Severity</p>
<p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#107C10;">{{TOTAL_LOW}}</p>
</td></tr>
</table>
</td>
<td>
<table width="100%" cellpadding="16" style="background:#ede0f5;border-radius:6px;border-left:4px solid #742774;">
<tr><td>
<p style="margin:0;font-size:11px;color:#4a1a5c;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">Total Findings</p>
<p style="margin:4px 0 0 0;font-size:32px;font-weight:700;color:#742774;">{{TOTAL_FINDINGS}}</p>
</td></tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<!-- APP OVERVIEW -->
<tr>
<td style="padding:28px 40px 0 40px;">
<h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">App Overview</h2>
<table width="100%" cellpadding="8" cellspacing="0" style="border-collapse:collapse;font-size:13px;">
<tr style="background:#f3f2f1;">
<td style="padding:8px 12px;font-weight:600;width:200px;">Screens</td>
<td style="padding:8px 12px;">{{SCREEN_COUNT}}</td>
<td style="padding:8px 12px;font-weight:600;width:200px;">Total Controls</td>
<td style="padding:8px 12px;">{{CONTROL_COUNT}}</td>
</tr>
<tr>
<td style="padding:8px 12px;font-weight:600;">Data Sources</td>
<td style="padding:8px 12px;" colspan="3">{{DATA_SOURCES}}</td>
</tr>
<tr style="background:#f3f2f1;">
<td style="padding:8px 12px;font-weight:600;">Flow Calls</td>
<td style="padding:8px 12px;">{{FLOW_CALLS}}</td>
<td style="padding:8px 12px;font-weight:600;">Connectors</td>
<td style="padding:8px 12px;">{{CONNECTORS}}</td>
</tr>
</table>
</td>
</tr>
<!-- CATEGORY SUMMARY -->
<tr>
<td style="padding:28px 40px 0 40px;">
<h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">Summary by Category</h2>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:13px;">
<tr style="background:#742774;color:#ffffff;">
<th style="padding:10px 12px;text-align:left;font-weight:600;">Category</th>
<th style="padding:10px 12px;text-align:center;font-weight:600;">🔴 High</th>
<th style="padding:10px 12px;text-align:center;font-weight:600;">🟡 Medium</th>
<th style="padding:10px 12px;text-align:center;font-weight:600;">🟢 Low</th>
</tr>
<!-- Repeat one <tr> per category. Alternate row background: #ffffff / #f3f2f1 -->
<tr style="background:#ffffff;">
<td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Performance</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{PERF_HIGH}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{PERF_MED}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{PERF_LOW}}</td>
</tr>
<tr style="background:#f3f2f1;">
<td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Coding Standards</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CODE_HIGH}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CODE_MED}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CODE_LOW}}</td>
</tr>
<tr style="background:#ffffff;">
<td style="padding:9px 12px;border-bottom:1px solid #edebe9;">App Design</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{DESIGN_HIGH}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{DESIGN_MED}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{DESIGN_LOW}}</td>
</tr>
<tr style="background:#f3f2f1;">
<td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Error Handling</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{ERR_HIGH}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{ERR_MED}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{ERR_LOW}}</td>
</tr>
<tr style="background:#ffffff;">
<td style="padding:9px 12px;border-bottom:1px solid #edebe9;">App Checker</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CHECKER_HIGH}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CHECKER_MED}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{CHECKER_LOW}}</td>
</tr>
<tr style="background:#f3f2f1;">
<td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Accessibility</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{A11Y_HIGH}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{A11Y_MED}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{A11Y_LOW}}</td>
</tr>
<tr style="background:#ffffff;">
<td style="padding:9px 12px;border-bottom:1px solid #edebe9;">Query Analysis</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{QUERY_HIGH}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{QUERY_MED}}</td>
<td style="padding:9px 12px;text-align:center;border-bottom:1px solid #edebe9;">{{QUERY_LOW}}</td>
</tr>
</table>
</td>
</tr>
<!-- ALL FINDINGS -->
<tr>
<td style="padding:28px 40px 0 40px;">
<h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">All Findings</h2>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
<tr style="background:#742774;color:#ffffff;">
<th style="padding:9px 10px;text-align:left;font-weight:600;width:28px;">#</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:72px;">Severity</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:100px;">Category</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:110px;">Area</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:130px;">Location</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:140px;">Code Found</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;">Finding & Recommendation</th>
</tr>
<!-- For each finding generate one <tr>. Alternate background #ffffff / #f9f8f7.
For High rows add style="border-left:3px solid #C50F1F;" to the first <td>.
For Medium rows add style="border-left:3px solid #F7630C;" to the first <td>.
For Low rows add style="border-left:3px solid #107C10;" to the first <td>. -->
<!-- EXAMPLE ROW (repeat for every finding):
<tr style="background:#ffffff;">
<td style="padding:8px 10px;border-bottom:1px solid #edebe9;border-left:3px solid #C50F1F;vertical-align:top;">1</td>
<td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;"><span style="background:#C50F1F;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;">High</span></td>
<td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;">Performance</td>
<td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;">N+1 Calls</td>
<td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;">Orders Screen — galOrders.Items</td>
<td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;font-family:Consolas,monospace;font-size:11px;color:#323130;background:#f3f2f1;">LookUp(Users, ID=ThisItem.CreatedBy)</td>
<td style="padding:8px 10px;border-bottom:1px solid #edebe9;vertical-align:top;"><strong>Finding:</strong> Each gallery row triggers a separate LookUp call (N+1).<br><strong>Fix:</strong> Use the Dataverse relationship: <code>ThisItem.'Created By'.'Full Name'</code></td>
</tr>
-->
</table>
</td>
</tr>
<!-- APP CHECKER RESULTS -->
<tr>
<td style="padding:28px 40px 0 40px;">
<h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">🔍 App Checker Results</h2>
<p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">Issues detected by the Power Apps App Checker engine (formula errors, performance warnings, deprecated features, data source issues).</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
<tr style="background:#742774;color:#ffffff;">
<th style="padding:9px 10px;text-align:left;font-weight:600;width:28px;">#</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:72px;">Severity</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:110px;">Rule / Area</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:160px;">Location</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:150px;">Code Found</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;">Message & Recommendation</th>
</tr>
<!-- For each App Checker finding generate one <tr> using the same row pattern as All Findings.
Category column is omitted here since all rows are "App Checker".
If no issues: single row with green badge and "App Checker returned no errors or warnings." -->
{{APPCHECKER_ROWS}}
</table>
</td>
</tr>
<!-- ACCESSIBILITY RESULTS -->
<tr>
<td style="padding:28px 40px 0 40px;">
<h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">♿ Accessibility Checker Results</h2>
<p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">Issues detected by the Power Apps Accessibility Checker (labels, color contrast, tab order, screen reader support, keyboard navigation).</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
<tr style="background:#742774;color:#ffffff;">
<th style="padding:9px 10px;text-align:left;font-weight:600;width:28px;">#</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:72px;">Severity</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:130px;">Rule / Area</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:160px;">Control</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:150px;">Property Value</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;">Message & Recommendation</th>
</tr>
<!-- For each Accessibility finding generate one <tr>.
If no issues: single row with green badge and "Accessibility Checker returned no errors." -->
{{ACCESSIBILITY_ROWS}}
</table>
</td>
</tr>
<!-- QUERY ANALYSIS -->
<tr>
<td style="padding:28px 40px 0 40px;">
<h2 style="margin:0 0 16px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">🗃️ Query & Data Source Analysis</h2>
<!-- OnStart Load Time KPIs -->
<h3 style="margin:0 0 8px 0;font-size:14px;font-weight:600;color:#323130;">App.OnStart — Estimated Load Time</h3>
<p style="margin:0 0 14px 0;font-size:12px;color:#605e5c;">Each external query is estimated at 100 ms. Queries inside <code>Concurrent()</code> count as a single 100 ms block regardless of how many queries are inside. Queries inside <code>ForAll()</code> are reported separately as N+1 risks.</p>
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td style="padding-right:12px;" width="33%">
<table width="100%" cellpadding="16" style="background:#dff6dd;border-radius:6px;border-left:4px solid #107C10;">
<tr><td>
<p style="margin:0;font-size:11px;color:#0e4a0b;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">✅ Best-Case Start Time</p>
<p style="margin:6px 0 4px 0;font-size:28px;font-weight:700;color:#107C10;">{{ONSTART_BEST_MS}} ms</p>
<p style="margin:0;font-size:11px;color:#0e4a0b;">All <code>If()</code> branches assumed <strong>false</strong> — conditional queries are skipped</p>
</td></tr>
</table>
</td>
<td style="padding-right:12px;" width="33%">
<table width="100%" cellpadding="16" style="background:#fff4ce;border-radius:6px;border-left:4px solid #F7630C;">
<tr><td>
<p style="margin:0;font-size:11px;color:#7a4100;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">⚠️ Worst-Case Start Time</p>
<p style="margin:6px 0 4px 0;font-size:28px;font-weight:700;color:#F7630C;">{{ONSTART_WORST_MS}} ms</p>
<p style="margin:0;font-size:11px;color:#7a4100;">All <code>If()</code> branches assumed <strong>true</strong> — every conditional query runs</p>
</td></tr>
</table>
</td>
<td width="33%">
<table width="100%" cellpadding="16" style="background:#fde7e9;border-radius:6px;border-left:4px solid #C50F1F;">
<tr><td>
<p style="margin:0;font-size:11px;color:#842029;font-weight:600;text-transform:uppercase;letter-spacing:.5px;">🔄 ForAll N+1 Risk (1 iteration)</p>
<p style="margin:6px 0 4px 0;font-size:28px;font-weight:700;color:#C50F1F;">{{ONSTART_FORALL_MS}} ms</p>
<p style="margin:0;font-size:11px;color:#842029;">Loop runs <strong>once</strong> (best case) — actual cost multiplies with record count (N × this value)</p>
</td></tr>
</table>
</td>
</tr>
</table>
<!-- OnStart Query Breakdown Table -->
<h3 style="margin:22px 0 10px 0;font-size:13px;font-weight:600;color:#323130;">App.OnStart Query Breakdown</h3>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
<tr style="background:#742774;color:#ffffff;">
<th style="padding:9px 10px;text-align:left;font-weight:600;width:32px;">#</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;">Query / Formula (≤ 120 chars)</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:120px;">Data Source</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:120px;">Execution Context</th>
<th style="padding:9px 10px;text-align:center;font-weight:600;width:90px;">Est. Time</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:100px;">Counted In KPI</th>
</tr>
<!-- For each query in App.OnStart generate one <tr>. Alternate background #ffffff / #f9f8f7.
Execution Context values: Sequential | Concurrent (block N) | If — <condition> | ForAll
Counted In KPI: Best + Worst | Worst Only (inside If) | N+1 Only (inside ForAll) -->
{{ONSTART_QUERY_ROWS}}
</table>
<!-- Per-Screen Data Source Operations & Duplicates -->
<h3 style="margin:28px 0 10px 0;font-size:13px;font-weight:600;color:#323130;">Data Source Operations & Duplicates per Screen</h3>
<p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">All Filter, LookUp, Search, ClearCollect, Collect, connector, and flow calls found in each screen's control formulas. Queries to the same data source within a screen are checked for identical or near-duplicate conditions.</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
<tr style="background:#742774;color:#ffffff;">
<th style="padding:9px 10px;text-align:left;font-weight:600;width:32px;">#</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:100px;">Screen</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:140px;">Control.Property</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:110px;">Data Source</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:80px;">Operation</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;">Filter Condition</th>
<th style="padding:9px 10px;text-align:center;font-weight:600;width:100px;">Duplicate?</th>
</tr>
<!-- For each operation generate one <tr>. Alternate background #ffffff / #f9f8f7.
Duplicate column: "🔴 Identical" | "🟡 Near-Duplicate" | "—" -->
{{DATASOURCE_OPS_ROWS}}
</table>
<!-- Named Formula Usage Per Screen -->
<h3 style="margin:28px 0 10px 0;font-size:13px;font-weight:600;color:#323130;">Named Formulas with Queries, Connectors & Flows — Usage per Screen</h3>
<p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">Named formulas defined in <code>App.Formulas</code> (in <code>App.pa.yaml</code>) that access external data or services, and how many times each is referenced across screens.</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
<tr style="background:#742774;color:#ffffff;">
<th style="padding:9px 10px;text-align:left;font-weight:600;width:150px;">Formula Name</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:140px;">Data Source / Connector / Flow</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:80px;">Operation</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;">Usage per Screen (screen: count, …)</th>
<th style="padding:9px 10px;text-align:center;font-weight:600;width:80px;">Total Uses</th>
</tr>
<!-- For each qualifying named formula generate one <tr>. Alternate background #ffffff / #f9f8f7.
Usage per Screen cell format: "Home Screen: 3, Orders Screen: 1" -->
{{NAMED_FORMULA_ROWS}}
</table>
<!-- Repeated Queries Per Screen -->
<h3 style="margin:28px 0 10px 0;font-size:13px;font-weight:600;color:#323130;">Repeated Queries per Screen</h3>
<p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">Queries to the same data source within a screen where filter conditions are identical or differ only in literal values. These are candidates for consolidation into a shared collection or parameterized formula.</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
<tr style="background:#742774;color:#ffffff;">
<th style="padding:9px 10px;text-align:left;font-weight:600;width:32px;">#</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:100px;">Screen</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:110px;">Data Source</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:80px;">Severity</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;">Queries Found (Control.Property — Filter Condition)</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:170px;">Recommendation</th>
</tr>
<!-- For each repeated-query group generate one <tr>. Alternate background #ffffff / #f9f8f7.
Severity badge: 🔴 Identical (redundant) | 🟡 Near-Duplicate (consolidate)
List each query in the group as a separate line inside the Queries Found cell. -->
{{REPEATED_QUERY_ROWS}}
</table>
<!-- Wide-Column Queries -->
<h3 style="margin:28px 0 10px 0;font-size:13px;font-weight:600;color:#323130;">Wide-Column Queries — Most Columns Returned (Descending)</h3>
<p style="margin:0 0 10px 0;font-size:12px;color:#605e5c;">Queries returning all or many columns without <code>ShowColumns()</code> restriction or ECS column selection. Restricting columns reduces payload size and improves load time.</p>
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse:collapse;font-size:12px;">
<tr style="background:#742774;color:#ffffff;">
<th style="padding:9px 10px;text-align:left;font-weight:600;width:32px;">#</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:72px;">Severity</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:110px;">Location</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;width:140px;">Control.Property</th>
<th style="padding:9px 10px;text-align:left;font-weight:600;">Query (≤ 120 chars)</th>
<th style="padding:9px 10px;text-align:center;font-weight:600;width:90px;">Est. Columns ↓</th>
</tr>
<!-- For each wide-column query generate one <tr>, sorted descending by Est. Columns.
Severity: High (≥ 20 cols) | Medium (10–19) | Low (< 10)
If schema not determinable write: "unknown — all columns"
Alternate background #ffffff / #f9f8f7 -->
{{WIDE_COLUMN_ROWS}}
</table>
</td>
</tr>
<!-- QUICK WINS -->
<tr>
<td style="padding:28px 40px 0 40px;">
<table width="100%" cellpadding="16" cellspacing="0" style="background:#dff6dd;border-radius:6px;border-left:4px solid #107C10;">
<tr><td>
<h2 style="margin:0 0 10px 0;font-size:15px;font-weight:600;color:#0e4a0b;">⚡ Quick Wins — Fix in < 5 Minutes</h2>
<!-- For each 🟢 Low / easy 🟡 Medium finding add a bullet: -->
<ul style="margin:0;padding-left:20px;line-height:1.7;">
<!-- <li><strong>[Area]</strong> — [one-line recommendation]</li> -->
{{QUICK_WINS_LIST}}
</ul>
</td></tr>
</table>
</td>
</tr>
<!-- HIGHEST IMPACT -->
<tr>
<td style="padding:20px 40px 0 40px;">
<table width="100%" cellpadding="16" cellspacing="0" style="background:#fde7e9;border-radius:6px;border-left:4px solid #C50F1F;">
<tr><td>
<h2 style="margin:0 0 10px 0;font-size:15px;font-weight:600;color:#842029;">🔴 Highest Impact Changes</h2>
<ul style="margin:0;padding-left:20px;line-height:1.7;">
<!-- <li><strong>[Area]</strong> — [one-line recommendation]</li> -->
{{HIGH_IMPACT_LIST}}
</ul>
</td></tr>
</table>
</td>
</tr>
<!-- MONITORING -->
<tr>
<td style="padding:28px 40px 0 40px;">
<h2 style="margin:0 0 12px 0;font-size:16px;font-weight:600;color:#323130;border-bottom:2px solid #edebe9;padding-bottom:8px;">📊 How to Monitor This App Going Forward</h2>
<p style="margin:0 0 10px 0;font-weight:600;color:#323130;">Option 1 — Monitor in Power Apps (PPAC)</p>
<ol style="margin:0 0 16px 0;padding-left:20px;line-height:1.8;color:#323130;">
Take microsoft/analyze-canvas-performance from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.