Most Canvas App UAT programs fail not because the app is bad, but because the feedback process collapses into chaos. This complete lesson shows you how to build a structured UAT coordination system using SharePoint Lists, Power Automate flows, and an embedded Canvas App feedback form — including automated notifications, version gating, and a formal sign-off workflow.

You've built the Canvas App. The screens look clean, the data connections work, and you've tested every button yourself at least a dozen times. Then you hand it off to real users — and within 48 hours, your inbox is a graveyard of contradictory feedback scattered across Teams messages, sticky notes photographed on someone's phone, and a spreadsheet that Karen from Operations started on her own initiative. Three people reported the same bug with different descriptions, two people want conflicting features, and nobody can agree on whether version 1.2 or 1.3 is what they're actually using right now.
This is the UAT problem. User Acceptance Testing for Canvas Apps fails not because the app is bad, but because the process around it collapses under the weight of unstructured human communication. The good news: you're already inside the Microsoft 365 ecosystem, which means SharePoint Lists and Power Automate are right there waiting to impose structure on the chaos. By the end of this lesson, you'll have a complete, working UAT coordination system built entirely in tools your organization already owns — one that tracks bugs with proper lifecycle management, automates tester notifications, enforces version discipline, and feeds you the signal you actually need to decide when an app is ready to ship.
What you'll learn:
You should be comfortable with:
Patch(), Filter(), and Navigate()If you haven't worked with Patch() to write records back to SharePoint, review the Power Apps data write operations lesson first.
Before we build anything, let's be explicit about the architectural choice here. You could track UAT feedback in an Excel file, a Planner board, or a third-party tool like Jira. The reason SharePoint Lists win for most Microsoft 365 shops doing Canvas App UAT is integration density — Power Automate can trigger on list item changes natively, Power Apps can read and write to lists with near-zero setup, and the permission model lets you expose a read view to stakeholders without handing them the keys to the whole system.
SharePoint Lists also give you something Excel doesn't: calculated columns and views that create live dashboards without building a reporting layer. A filtered view showing all "Open" bugs for version 1.4 is a two-minute configuration, not a pivot table someone has to remember to refresh.
The tradeoff is that SharePoint Lists have column type limitations (no true relational foreign keys, limited formula complexity) and can get slow above ~5,000 items if you haven't set indexed columns properly. For a typical Canvas App UAT cycle with 10-50 testers over 4-8 weeks, you'll land comfortably under those limits.
Your SharePoint tracking system needs two lists: a Bug/Feedback Tracker list and an App Versions list. Keeping them separate lets you maintain a clean version history independent of bug volume.
Create a SharePoint List called UAT_AppVersions. Add these columns:
| Column Name | Type | Notes |
|---|---|---|
| Title | Single line text | Auto-created; use for version number e.g. "v1.4" |
| ReleaseDate | Date and Time | When this build was deployed to testers |
| ReleaseNotes | Multiple lines of text | Plain text, what changed in this build |
| Status | Choice | Options: Draft, Active, Superseded, Approved |
| AppURL | Hyperlink | Direct link to this Canvas App version |
| ApprovedBy | Person or Group | Who formally signed off |
| ApprovalDate | Date and Time | When sign-off happened |
The Status field does a lot of work here. Only one version should ever be Active at a time — we'll enforce that with a Power Automate flow later. When a new version becomes Active, the previous one moves to Superseded. When UAT is complete, the final version moves to Approved.
Create a SharePoint List called UAT_FeedbackTracker. This is your main workhorse. Add these columns:
| Column Name | Type | Notes |
|---|---|---|
| Title | Single line text | Short summary — "Submit button unresponsive on tablet" |
| FeedbackType | Choice | Bug, Enhancement Request, Question, Usability Issue |
| Severity | Choice | Critical, High, Medium, Low |
| Status | Choice | New, Triaged, In Progress, Fixed, Verified, Closed, Won't Fix |
| AppVersion | Lookup | Lookup to UAT_AppVersions → Title column |
| SubmittedBy | Person or Group | Auto-populated from the submitting user |
| SubmissionDate | Date and Time | Auto-set on creation |
| Description | Multiple lines of text | Full reproduction steps |
| ExpectedBehavior | Multiple lines of text | What should happen |
| ActualBehavior | Multiple lines of text | What actually happens |
| DevNotes | Multiple lines of text | Developer response/fix notes |
| AssignedTo | Person or Group | Developer or team member owning this item |
| ResolvedDate | Date and Time | When status moved to Fixed or Closed |
| VerifiedBy | Person or Group | Tester who confirmed the fix |
| TestDevice | Choice | Desktop Browser, iOS Tablet, Android Tablet, Mobile, Other |
| Attachments | Attachments | Screenshots — enable at list level |
Index the Status, AppVersion, and Severity columns immediately after creation. Go to List Settings → Indexed Columns → Create a new index for each. This prevents throttling if your list grows beyond 5,000 items and makes filtered views performant.
Why capture Expected vs. Actual Behavior separately? Because "the button doesn't work" is not a bug report — it's a complaint. Forcing testers to articulate what they expected to happen surfaces the real specification gap. You'll catch cases where the app is technically working but the UX violated a user's reasonable expectation. Those are design bugs, not code bugs, and they need different fixes.
The single biggest friction point in UAT is the distance between "I notice something wrong" and "the developer knows about it." If testers have to leave the app, open SharePoint, find the list, and fill out a form — they won't. The fix is embedding feedback submission directly inside the Canvas App being tested.
In your Canvas App, add a new screen called FeedbackScreen. On every other screen in the app, add a small icon button — a speech bubble or flag icon works well — positioned consistently in a corner. Set its OnSelect property to:
Navigate(FeedbackScreen, ScreenTransition.Cover)
Store the current screen name before navigating so your feedback form can pre-populate which screen the issue was found on:
Set(varSourceScreen, App.ActiveScreen.Name);
Navigate(FeedbackScreen, ScreenTransition.Cover)
On FeedbackScreen, you're not using a standard Power Apps Form control connected to SharePoint. You're building individual controls and using Patch() to write. This gives you better control over default values and calculated fields.
Add these controls:
A Text Input for the issue summary:
Set Default to "" and HintText to "Brief summary of the issue". Name it txtSummary.
A Dropdown for Feedback Type:
Items: ["Bug", "Enhancement Request", "Question", "Usability Issue"]
Name it ddFeedbackType.
A Dropdown for Severity:
Items: ["Critical", "High", "Medium", "Low"]
Name it ddSeverity.
A Text Input for Description (multiline):
Set Mode to TextMode.MultiLine. Name it txtDescription.
A Text Input for Expected Behavior (multiline):
Name it txtExpected.
A Text Input for Actual Behavior (multiline):
Name it txtActual.
A Dropdown for Test Device:
Items: ["Desktop Browser", "iOS Tablet", "Android Tablet", "Mobile", "Other"]
Name it ddDevice.
A Label showing the current App Version:
We'll pull this from a global variable set at app start. Name the label lblCurrentVersion and set its Text to varCurrentVersion.Title (we'll define this variable next).
In the App.OnStart property, add:
Set(
varCurrentVersion,
LookUp(
UAT_AppVersions,
Status = "Active"
)
);
Set(varSourceScreen, "");
This loads the single Active version record at app startup. If no version is Active, varCurrentVersion will be blank — that's your signal that the version list needs updating before testers start.
Add a guard clause. On your app's first screen, add a label or banner that displays when
IsBlank(varCurrentVersion):Visible: IsBlank(varCurrentVersion) Text: "⚠️ No active UAT version configured. Contact your administrator."This prevents testers from submitting feedback that can't be linked to a version.
Add a Button labeled "Submit Feedback" and set its OnSelect to:
If(
IsBlank(txtSummary.Text) || IsBlank(txtDescription.Text),
Notify("Please complete Summary and Description before submitting.", NotificationType.Warning),
Patch(
UAT_FeedbackTracker,
Defaults(UAT_FeedbackTracker),
{
Title: txtSummary.Text,
FeedbackType: { Value: ddFeedbackType.Selected.Value },
Severity: { Value: ddSeverity.Selected.Value },
Status: { Value: "New" },
AppVersion: varCurrentVersion,
Description: txtDescription.Text,
ExpectedBehavior: txtExpected.Text,
ActualBehavior: txtActual.Text,
TestDevice: { Value: ddDevice.Selected.Value },
SubmissionDate: Now(),
SubmittedBy: {
DisplayName: User().FullName,
Email: User().Email
}
}
);
If(
IsEmpty(Errors(UAT_FeedbackTracker)),
Notify("Feedback submitted successfully. Thank you!", NotificationType.Success);
Reset(txtSummary);
Reset(txtDescription);
Reset(txtExpected);
Reset(txtActual);
Navigate(varSourceScreen, ScreenTransition.UnCover),
Notify("Submission failed. Please try again or contact support.", NotificationType.Error)
)
)
A few things to note in this formula. The { Value: ... } syntax is required for Choice columns — Power Apps needs the object structure, not a raw string. The SubmittedBy field being a Person column requires the DisplayName and Email structure; if you just pass User().Email as a string it won't resolve to a person record correctly in SharePoint. And navigating back to varSourceScreen after submission returns the tester to exactly where they were, which is the UX that actually gets used.
Your SharePoint list is collecting feedback. Now you need the right people to hear about it without everyone getting notified about everything.
Create a new automated cloud flow triggered by "When an item is created" on UAT_FeedbackTracker.
Add a Condition action checking:
Severity is equal to Critical
If Yes, add a Send an email (V2) action (or Post a message in Microsoft Teams, which is usually better for dev teams):
To: Your development lead's email, or a distribution group
Subject: [CRITICAL BUG] Severity: Critical — [Title]
Body:
A critical bug has been submitted in the Canvas App UAT tracker.
Summary: [Title]
Version: [AppVersion.Title via dynamic content lookup]
Submitted By: [SubmittedBy.DisplayName]
Device: [TestDevice]
Description:
[Description]
Expected: [ExpectedBehavior]
Actual: [ActualBehavior]
View and triage this item: [Link to item]
For the No branch (non-critical), you can choose to do nothing — or add a Teams adaptive card notification to a UAT channel on a daily digest schedule instead. Flooding developers with every low-severity submission is the fastest way to get flows ignored.
Build a Teams-first culture for UAT. Instead of email, use the "Post adaptive card and wait for a response" Teams action for triage. The developer gets a card in Teams showing the bug details, and can click "Acknowledge" or "Triage" buttons directly in the card. This keeps the conversation inside the tool your team is already in, and the response updates the SharePoint item automatically.
Testers lose confidence in UAT programs when they submit bugs and hear nothing. Even a simple "your bug is being worked on" message dramatically increases tester engagement.
Create a second automated flow triggered by "When an item is modified" on UAT_FeedbackTracker.
Add a Condition:
Trigger outputs → body/Status/Value → is not equal to → [Previous status]
Getting the "previous status" in Power Automate requires a workaround. Use the Get item action before the status-checking condition to retrieve the SharePoint item, then use triggerOutputs()?['body/Status/Value'] compared against what you expect. A cleaner approach: add a PreviousStatus hidden column to your list, and update it to match the current Status as part of your developer workflow. Then your condition becomes:
Status (current) is not equal to PreviousStatus
When this condition is true, send an email to the SubmittedBy email address:
Subject: Update on your UAT feedback: [Title]
Body:
Hi [SubmittedBy DisplayName],
Your feedback item has been updated.
Item: [Title]
New Status: [Status]
Updated By: [Modified By]
Developer Notes: [DevNotes]
Thank you for helping us improve the app.
Only send when Status changes to: In Progress, Fixed, or Closed. Do not email on Triaged — that's an internal state, not a tester-facing milestone.
This flow enforces the "only one Active version" rule. When someone sets a version record to Active in UAT_AppVersions, this flow automatically sets all other version records to Superseded.
Create an automated flow triggered by "When an item is modified" on UAT_AppVersions.
Add a Condition: Status equals "Active"
If Yes:
UAT_AppVersions with filter: Status eq 'Active' and ID ne [Trigger ID]SupersededThen add a second action outside the loop: Update item on the triggering item to set ReleaseDate to the current UTC time (if not already set).
Finally, Get items from UAT_FeedbackTracker where Status equals New, and send a summary email to your UAT coordinator:
New version [Title] is now active for UAT testing.
[Count] open feedback items carried over from previous version.
Release Notes: [ReleaseNotes]
Test URL: [AppURL]
This gives the UAT coordinator an instant inventory of unresolved issues at version launch.
UAT without release discipline is just beta testing that never ends. You need a formal cycle structure that moves versions forward with clear criteria.
Define these stages explicitly with your stakeholders before testing begins:
Stage 1 — Triage (Days 1-2 post-release): All new submissions get reviewed. Each item gets a Severity and an AssignedTo. Items that are duplicates or out of scope get closed with a note.
Stage 2 — Fix Sprint (Days 3-7): Developers work through Triaged items. Status moves from Triaged → In Progress → Fixed. Critical and High items must be resolved before Stage 3.
Stage 3 — Verification (Days 8-10): Testers re-test Fixed items using the same device/context where they found them. Status moves Fixed → Verified or Fixed → Reopened (if the fix didn't work).
Stage 4 — Sign-Off (Day 11+): UAT coordinator reviews: are all Critical and High items Verified or Won't Fix? If yes, initiate formal approval. If no, cycle back to Stage 2.
Create a manually triggered flow (not automated — this is initiated by the UAT coordinator when they believe the app is ready):
Trigger: Manually trigger a flow, with an input field for "Version Number" (text).
Actions:
Get items from UAT_FeedbackTracker filtering for the specified version where Status is not in [Verified, Closed, Won't Fix] and Severity is in [Critical, High]
Condition: Item count is greater than 0
If Yes (unresolved high-severity items exist): Send a failure notification to the coordinator. Do not proceed.
If No (all high-severity items resolved): Start and wait for an approval
UAT Sign-Off Request: Canvas App [Version Number]Condition: Approval outcome equals "Approve"
If Approved: Update item in UAT_AppVersions — set Status to Approved, set ApprovedBy to the approval responder, set ApprovalDate to now. Then send an all-testers notification email confirming UAT completion.
If Rejected: Update version status back to Active (it never left), notify the UAT coordinator with the rejection comments.
Use "Approve/Reject - Everyone must approve" when you have multiple business stakeholders who need to sign off. Use "Approve/Reject - First to respond" when you have a single decision-maker. Mixing these up is a common cause of approval flows that stall indefinitely.
Your UAT coordinator and project manager need visibility without digging into list data. Build them a read-only Dashboard screen inside your Canvas App, or use a SharePoint page with list web parts — the SharePoint approach requires zero Power Apps work and is often faster to deliver.
On a SharePoint site page, add the following web parts:
List web part 1: UAT_FeedbackTracker filtered to current Active version, grouped by Status. This gives the at-a-glance "how many bugs are in each state."
List web part 2: UAT_FeedbackTracker filtered to Severity = Critical OR High, Status ≠ Verified/Closed. This is your "blocking items" board.
List web part 3: UAT_FeedbackTracker filtered to Status = New. This is the triage queue.
Configure each web part to show only the columns relevant to that context. The Critical/High view should show Title, Status, AssignedTo, and SubmissionDate. The triage queue should show Title, FeedbackType, Severity, SubmittedBy, and SubmissionDate.
If you want a more polished experience, add a DashboardScreen to your Canvas App accessible only to the dev team and UAT coordinator (use User().Email comparisons against a known admin email collection to gate access).
Add a Gallery showing open items grouped by severity:
Items: Sort(
Filter(
UAT_FeedbackTracker,
AppVersion.Id = varCurrentVersion.Id,
Status.Value <> "Verified",
Status.Value <> "Closed",
Status.Value <> "Won't Fix"
),
Severity.Value,
SortOrder.Ascending
)
Add four Label controls showing counts by severity:
// Critical count
Text: "Critical: " & CountIf(
UAT_FeedbackTracker,
AppVersion.Id = varCurrentVersion.Id,
Severity.Value = "Critical",
Status.Value <> "Verified"
)
Repeat for High, Medium, and Low. These give your coordinator the instant status check they need in a morning standup without opening SharePoint.
Build a complete UAT coordination system for a fictional Canvas App called FieldServiceTracker — an app used by field technicians to log service visits. You'll be coordinating UAT across 15 testers who use a mix of iOS tablets and desktop browsers.
Step 1: Create the SharePoint infrastructure
Create both SharePoint Lists as specified in the schema section. Add the indexed columns on Status, AppVersion, and Severity in UAT_FeedbackTracker. Create your first version record in UAT_AppVersions: Title = "v1.0", Status = Active, ReleaseDate = today, ReleaseNotes = "Initial UAT build — core service log entry workflow only."
Step 2: Build the feedback submission screen
Open or create a Canvas App connected to your SharePoint lists. Build FeedbackScreen with all the controls described above. Set App.OnStart to load varCurrentVersion. Test by submitting one record of each FeedbackType and confirm the items appear in SharePoint with correct Severity, Version lookup, and SubmittedBy values.
Step 3: Create the Critical Bug Alert flow
Build Flow 1 as described. Test it by submitting a feedback item with Severity = Critical from the app. Verify you receive the Teams or email notification within 2 minutes.
Step 4: Simulate a bug lifecycle
Manually change one feedback item through the full lifecycle: New → Triaged → In Progress → Fixed → Verified. Verify that your Status Change Notification flow fires on the Fixed and Verified transitions. Check that the submitter email address receives the notification.
Step 5: Initiate the Sign-Off flow
First, manually set all your test items to Verified status in SharePoint. Then trigger the Sign-Off flow manually, entering "v1.0" as the version number. Approve the request from the approval notification. Verify that the UAT_AppVersions list reflects Approved status with the correct approver and date.
Step 6: Release v1.1
Create a new version record: Title = "v1.1", Status = Active. Verify that the Version Activation Gate flow automatically sets v1.0 to Superseded and sends a new-version notification. Verify that varCurrentVersion in the Canvas App now reflects v1.1 when the app is refreshed.
"The Lookup column for AppVersion isn't populating correctly in Patch()"
The most common mistake. When patching a Lookup column in Power Apps, you need to pass an object with Id (and optionally Value) — not just the title string. If varCurrentVersion is a full record from UAT_AppVersions, passing it directly to the Lookup field usually works, but if you're constructing the object manually, use:
AppVersion: { Id: varCurrentVersion.ID, Value: varCurrentVersion.Title }
Note the uppercase ID — SharePoint's primary key field is ID in Power Apps, not Id. This inconsistency causes errors that are maddeningly hard to spot.
"My Power Automate flow triggers on every modification, spamming notifications"
You didn't add a condition checking whether the specific field you care about actually changed. The "When an item is modified" trigger fires on any column edit, including metadata fields SharePoint updates internally. Always add a condition at the top of your flow checking the specific value you care about before taking any action.
"Testers are submitting feedback against the wrong version"
This happens when App.OnStart is cached and the user hasn't fully restarted the app since a new version went Active. Add a version check on your main screen that compares varCurrentVersion.ID to a fresh lookup:
OnVisible: If(
varCurrentVersion.ID <> LookUp(UAT_AppVersions, Status = "Active").ID,
Set(varCurrentVersion, LookUp(UAT_AppVersions, Status = "Active"))
)
Running this OnVisible on your home screen refreshes the version context each time a tester navigates back to start, without requiring a full app reload.
"The Sign-Off flow runs but finds 0 open items even though there are clearly open bugs"
Check your filter query syntax in the Get Items action. Power Automate's OData filter for SharePoint Choice columns uses the format Status/Value eq 'Open' — not Status eq 'Open'. For Lookup columns: AppVersion/Title eq 'v1.0'. Getting this syntax wrong produces a filter that either returns everything or nothing, with no error message.
"The approval is stalled — nobody received the approval request"
Check the approver email addresses are valid Microsoft 365 accounts, not external guest accounts (approval flows don't always resolve to external guests correctly). Also check whether your organization has disabled the Power Automate approval connector — some IT departments do this. If approvals are blocked, use a workaround: write a "PendingApproval" record to a SharePoint list and use a Canvas App with a button that a specific user can click to trigger the approval outcome flow.
"The Severity count labels on the dashboard are slow to load"
CountIf() against a delegable data source should be fine for typical UAT volumes, but if you're seeing delays, it's likely because Power Apps is falling back to non-delegated execution. Make sure your filter conditions use only delegable operators for SharePoint (=, <>, And, Or with simple field comparisons). Avoid in operators and custom functions inside the filter. If delegation is the issue, load the filtered collection into a ClearCollect on DashboardScreen.OnVisible and run CountIf against the local collection instead.
You now have a complete UAT coordination system that solves the real problems: feedback scattered across channels, version confusion, no tester follow-through, and no formal sign-off process. Let's recap what you built:
The pattern you've built here isn't just for UAT. The same architecture — SharePoint as structured tracker, Power Automate as notification and workflow engine, Canvas App as low-friction submission interface — applies to change request management, incident reporting, and operational checklists. Once you've internalized this pattern, you'll find yourself reaching for it repeatedly.
Where to go next:
UAT isn't a phase you rush through at the end. Done well, it's how you build user trust in a product that's going to be part of their daily workflow. The system you've built today makes that possible without requiring everyone to be disciplined — it enforces the discipline automatically.