Canvas or model-driven? Think "build from scratch" vs. "assemble a kit"
Power Apps actually means two quite different tools that share one name. The first question almost every beginner asks is: "which one do I build?" Here's the framework I walk clients through, in plain terms.
A canvas app is like building furniture from raw wood with total creative freedom. You start with a blank screen and decide exactly where every button, image, and text box goes, and exactly what happens when someone taps them. Nothing is decided for you, which is powerful, but it means building every screen by hand.
A model-driven app is more like assembling furniture from a structured kit, with numbered steps. You describe your data first (what a "Case" record looks like, what a "Contact" record looks like, how they relate), and the platform automatically builds the forms, lists, and menus for you, in a consistent style. You give up some layout freedom, but you get a working app fast, and it stays consistent as it grows.
So which fits your situation? Model-driven is the right call when your app lives entirely inside Dataverse (Microsoft's built-in business database that stores records like accounts, contacts, and cases) and your users are internal staff who'll benefit from a consistent experience across many connected tables. Picture an internal case-management app touching Cases, Accounts, Contacts, and a custom Escalations table — getting the relationships and rules right matters far more than pixel-perfect layout.
Canvas apps make sense when the look and feel of the screen itself is the point — a mobile app for a field technician to snap inspection photos, a kiosk sign-in screen, or an app pulling together data from Dataverse, SharePoint, SQL Server, and an outside web service on one screen. Canvas hands you full control over layout and flow, at the cost of building every screen and button yourself.
It's also completely normal, and often the smartest choice, to combine both. A model-driven app can have a canvas app embedded inside one of its forms, or as a custom page, for the one screen the standard form can't handle well, like a drag-and-drop scheduling board.
Inside a canvas app: screens, navigation, and "memory"
A canvas app is a set of screens, similar to slides in a slideshow, each filled with controls like buttons, labels, and image boxes. Instead of traditional programming code, you write formulas that look a lot like Excel formulas — if a cell's value changes, anything depending on it updates automatically, the same way a spreadsheet recalculates itself. Moving between screens uses the Navigate function, usually attached to a button's "when this is tapped" property (OnSelect):
Navigate(
ScreenCaseDetail,
ScreenTransition.Fade,
{SelectedCaseID: ThisItem.CaseID}
)
In plain English: "go to the case detail screen, fade in nicely, and carry the ID of the case the user just tapped along with you."
An app also needs to remember things temporarily as the user moves around — that's what variables are for, and they come in two flavors. A context variable (created with UpdateContext) is like a sticky note only the current screen can read — useful for "is this panel expanded right now." A global variable (created with Set) is like a sticky note pinned to a board every screen can see — useful for "which case did the user just click on," which the next screen needs to know too.
// Global - visible app-wide, like a note pinned where every screen can see it
Set(varSelectedCaseID, ThisItem.CaseID);
// Context - visible on this screen only, like a private sticky note
UpdateContext({locIsPanelOpen: true})
A handful of formulas you'll use constantly
Once you get comfortable with a small set of formulas, most day-to-day canvas app building becomes routine:
- Filter — shows only rows that match a condition, the same way filtering your inbox to "unread only" hides the rest without deleting it:
Filter(Cases, Status = "Active" && Owner = varCurrentUser). - LookUp — finds one matching record, like looking up a single phone number by someone's name:
LookUp(Accounts, AccountID = ThisItem.AccountID).Name. - Left / Mid / Right — grab a piece of a longer piece of text, like cutting a specific length off a piece of string — handy for pulling the first few digits out of a code, or shortening a label so it fits on screen.
- AddColumns — adds a temporary, calculated column onto a list of records without changing the original data, similar to jotting an extra column of notes onto a printed report without touching the original file — for example, a "DaysOpen" column that isn't stored anywhere, just calculated for display.
- Patch — saves a new record or updates an existing one, like editing one row in a shared spreadsheet and hitting save:
Patch(Cases, ThisItem, {Status: "Resolved"}).
AddColumns(
Filter(Opportunities, StatusCode = "Open"),
"DaysOpen",
DateDiff(CreatedOn, Today(), Days)
)
Read that as: "take only the open opportunities, and for each one, work out how many days it's been open, then show that as an extra column."
What happens when something goes wrong
Canvas apps don't have a traditional "try this, and if it breaks, catch the error" structure, but IfError does almost the same job. Think of it like a building's backup generator: if the main power (your save action) fails, the generator (your fallback message) kicks in automatically instead of leaving everyone in the dark. Wrap a risky action, usually a Patch or a call to another system, and tell the app what to do in both cases:
IfError(
Patch(Cases, ThisItem, {Status: "Resolved"}),
Notify("Couldn't save the case. Please try again.", NotificationType.Error),
Notify("Case updated.", NotificationType.Success)
)
Notify pops up a small banner message on screen, similar to a toast notification on your phone. Use it consistently rather than letting failures happen silently — if a user taps "save" and nothing confirms it worked, they'll quickly stop trusting the app, the same way you'd worry if a text message showed no checkmark at all.
Habits that keep your app from turning into a mess
Left unchecked, canvas app formulas can turn into a tangle that's hard for anyone, including future-you, to follow. A few habits, adopted early, pay off on every project:
- Consistent naming — prefix variables (
varfor global,locfor context,colfor collections) and controls (btn,lbl,gal) the same way you'd label boxes clearly when moving house, so you know what's inside without opening every one. - Avoid deeply nested formulas — a formula with five layers of conditions buried inside each other is like a recipe written as one giant run-on sentence instead of numbered steps: correct, but painful to follow. Break logic into clearly named variables set when the app or screen loads, or move it into a reusable component.
- Componentize repeated UI — if the same status badge or card layout shows up on three screens, build it once as a reusable component, the way a cookie cutter lets you stamp out the same shape repeatedly instead of hand-drawing it every time.
- Centralize collections — load reference data once, when the app starts, into a local collection rather than re-fetching the same lookup list from every screen — one big grocery run for the week instead of a separate trip for every meal.
Power Pages: opening a door for people outside your company
Power Pages (previously called Power Apps portals) shows Dataverse data to people outside your team — customers checking an order status, vendors submitting an invoice, or the public applying for a permit. If a model-driven app is your company's internal back office, Power Pages is the public storefront built on the same underlying data.
The important shift is around access: internally, staff get broad default access based on their security role, similar to an employee badge that opens most doors in the building. On a public Power Pages site there's no such default access — everything a visitor can see or do has to be explicitly unlocked through a table permission and a web role, like a storefront where every internal door stays locked unless you specifically hand out a key.
Designing a model-driven app: forms, views, and a bit of code
Forms and views
Model-driven apps are built from familiar pieces: main forms (the detailed record screen), quick view or quick create forms (compact popups), and views (saved lists, shared or personal). Designing a good form is mostly about grouping related fields into clear tabs and sections, using business rules to show, hide, or require fields, and adding sub-grids so related records show up right on the page instead of forcing users to click away.
Client API and form context basics
Sometimes the built-in, no-code tools can't quite do what you need, and that's when a small bit of JavaScript against the platform's Client API fills the gap. Nearly every form script starts the same way, by grabbing hold of the current form:
function onLoad(executionContext) {
var formContext = executionContext.getFormContext();
var statusAttr = formContext.getAttribute("statuscode");
if (statusAttr.getValue() === 100000001) {
formContext.getControl("description").setDisabled(true);
}
}
In plain English: when the form loads, the platform hands your code a small toolbox called executionContext. From it you pull out formContext, your entry point to everything on the current form — its fields, buttons, and navigation. This example checks a status field, and if it matches a specific value, locks the description field so it can't be edited.
Business rules vs. business process flows: not the same thing
These two names get mixed up constantly, so it's worth being precise. A business rule is like an automatic seatbelt warning light in a car: the instant a condition becomes true on the screen you're looking at ("Priority is High"), it reacts immediately, no matter where in the process you are. A business process flow (BPF) is more like the numbered checklist a driving instructor hands you for a road test: it guides you through an entire process, step by step, and shows you exactly which stage you're on.
| Business Rules | Business Process Flows |
|---|---|
| React instantly to a single field: show/hide it, require it, set its value, or check it's valid | Walk a user through a multi-stage process, sometimes across more than one type of record |
| Work on one form at a time (and can optionally also run on the server) | Show up as a row of numbered stages across the top of the form |
| Have no idea of "stage" or "progress" — they just react to conditions | Enforce or suggest the right order of steps to move a record toward completion |
| Good for: "if Priority = High, require an Escalation Reason" | Good for: Lead → Opportunity → Quote → Order, a whole sales journey |
In practice, a well-designed table often uses both together: a BPF to guide the overall journey, with business rules underneath doing the fine-grained field checking at each stage.
A good next step: the PL-900 certification
If you're learning canvas apps, model-driven apps, and the basics of Power Automate and Power BI around the same time, the PL-900 (Microsoft Power Platform Fundamentals) certification is a natural checkpoint. It doesn't demand deep expertise in any single product — it confirms you understand how the pieces fit together as a whole, which is exactly the big-picture understanding this page is trying to build.