After years of writing SPFx web parts and C# event receivers, my first canvas app felt like cheating — I dragged a gallery onto a screen, pointed it at a list, and had a working mobile app before lunch. But "low-code" is not "no-thought." The developers who build canvas apps that scale are the ones who understand the data layer, Power Fx, and delegation. Here's the mental model I wish I'd had on day one.
Canvas vs model-driven — pick the right one
Power Apps gives you two app types, and choosing wrong is the most expensive mistake you can make:
- Canvas apps — pixel-perfect, drag-and-drop UI over any data source (SharePoint, Dataverse, SQL, Excel, 1,000+ connectors). You control every control's position and behavior. Best for task-focused, mobile-first experiences.
- Model-driven apps — the UI is generated from your Dataverse data model and forms. You design data and business rules; the app shapes itself. Best for complex, relationship-heavy line-of-business systems.
This article is about canvas, because that's where most people start — and where Power Fx lives.
The anatomy of a canvas app
A canvas app is a tree of screens, each holding controls (galleries, forms, labels, buttons, inputs). Controls have properties, and almost every property is set with a Power Fx formula — a declarative, Excel-like expression that recalculates automatically when its inputs change.
Browse screen: a Gallery bound to a data source, with a row template of avatar, title, subtitle, and a status badge.
Connecting data
A canvas app talks to data through connectors. Add the SharePoint connector, point it at a site, pick the Contracts list, and it becomes a data source you reference by name. The gallery's Items property is just a formula that returns a table:
// Gallery1.Items — show approved contracts, newest first
SortByColumns(
Filter(Contracts, ContractStatus.Value = "Approved"),
"Modified",
SortOrder.Descending
)
Inside the gallery template you reference the current row with ThisItem:
// Title label inside the gallery
ThisItem.Title
// Subtitle label
ThisItem.ContractType.Value & " · " & Text(ThisItem.ContractValue, "$#,##0")
ContractStatus.Value, not ContractStatus. Forgetting .Value is the single most common "why is my filter broken?" issue for newcomers.
Delegation — the concept that separates toy apps from real ones
Here is the most important thing in this entire article. Power Apps does not pull your whole list to the device — that wouldn't scale. Instead it tries to delegate the work (filtering, sorting) to the data source, which processes it server-side and returns only matching rows. But not every function or operator can be delegated, and the set differs per connector.
When you write a formula the source can't delegate, Power Apps silently falls back to pulling only the first 500 rows (raisable to 2,000 in app settings) and processing them locally. Your app looks fine in testing against a small list, then quietly returns wrong results in production.
=, StartsWith, and basic Filter/Sort on indexed columns delegate; functions like Search() on some column types, or filtering across a many-lookup, often do not. Design your queries around the delegable set, and keep working sets under the row limit.
Editing data: forms and Patch
For straightforward edit screens, the Edit Form control does the heavy lifting — bind its DataSource and Item, drop in cards for each field, and call SubmitForm on a button. The form handles validation, required fields, and the write-back:
// "Save" button — OnSelect
SubmitForm(EditForm1)
// Form OnSuccess — go back and confirm
Notify("Contract saved", NotificationType.Success);
Back()
When you need precise control — write specific fields, create-or-update logic, no form control — use Patch directly:
// Approve the selected contract without a form
Patch(
Contracts,
Gallery1.Selected,
{ ContractStatus: { Value: "Approved" }, ApprovedOn: Now() }
)
State: variables, context, and collections
Canvas apps are reactive, so you reach for explicit state less than you'd think — but when you do, there are three tools, and mixing them up causes real confusion:
| Tool | Scope | Set with |
|---|---|---|
| Global variable | Whole app | Set(varUser, User()) |
| Context variable | Single screen | UpdateContext({ showPanel: true }) |
| Collection | Whole app (in-memory table) | Collect(colCart, ThisItem) |
// App.OnStart — load the current user once, globally
Set(gblCurrentUser, User());
// Pass a record to a detail screen
Navigate(DetailScreen, ScreenTransition.Cover, { selected: Gallery1.Selected })
.Selected and direct references over stuffing everything into variables. The more you let Power Fx recalculate declaratively, the less stale-state debugging you'll do. Reach for Set/UpdateContext only for genuine UI state (a toggled panel, a loading flag).
Publishing
Save and Publish promotes your edits to the live version users run; Save alone only checkpoints your draft. Share the app with users or a security group, and — critically — they also need access to the underlying data source (the SharePoint list, the Dataverse table). The app is just the UI; permissions still live with the data.
The takeaway
A canvas app is a tree of controls whose properties are Power Fx formulas over connected data. Master four things — the data connector, ThisItem/Selected binding, delegation, and Patch/SubmitForm — and you can build genuinely production-grade apps fast. Skip delegation in particular, and you'll ship something that demos beautifully and fails the moment the list grows past 500 rows. Low-code rewards the developer who still thinks like one.