Learn how to build a complete model-driven app from scratch — designing tables, configuring forms with subgrids and quick views, creating operational views, and building a site map that organizes everything into a deployable, role-aware application. This is the deep-dive lesson that teaches you to think in model-driven architecture, not just click through a designer.

Imagine you've just inherited responsibility for your organization's field service operation. Technicians are logging jobs in spreadsheets, managers are emailing status updates, and nobody can answer the question "which open tickets are assigned to which technician in which region?" without three phone calls and a prayer. You know the data model you need — jobs, technicians, customers, regions — but what you need is an app that puts that data in front of the right people, in the right structure, with the right controls.
This is exactly the scenario where model-driven apps shine. Unlike canvas apps, where you hand-craft every pixel of the UI, model-driven apps generate their interface from your data model. You define the tables, the relationships, the forms, and the views — and the Power Apps framework renders a consistent, responsive, role-aware application that works on desktop, tablet, and mobile without you writing a single line of layout code. The trade-off is real: you give up granular UI control in exchange for dramatically faster development, enterprise-grade accessibility, and built-in integration with Dataverse features like business rules, business process flows, and role-based security.
By the end of this lesson, you will have built a complete, deployable model-driven app for a field service scenario. You'll understand not just how to click through the designer, but why each structural decision matters — and where those decisions create downstream consequences you'll regret in production.
What you'll learn:
Before you start, you should be comfortable with the following:
The single biggest mistake first-time model-driven app builders make is opening the app designer immediately. Don't. Model-driven apps are metadata-driven — they render their UI based on configurations stored in Dataverse metadata. That means every structural decision ripples outward. If you build your forms first and then realize your table is wrong, you're not adjusting a control's X/Y position; you're restructuring a schema that other apps, flows, and integrations may already depend on.
Think of a model-driven app as four nested layers:
The app definition is what you publish. Everything inside it — site map, forms, views — is metadata that the Unified Interface shell interprets at runtime. This architecture is why model-driven apps can adapt to a user's security role without any conditional logic in your form: the framework simply doesn't render components the user doesn't have access to.
Key insight
Forms and views in model-driven apps are shared across apps. If you modify a Main Form on the Account table, that change affects every model-driven app in your environment that includes Account. This is the most common source of accidental regressions in enterprise deployments. Always work in a solution, and always check which apps reference a component before editing it.
For this lesson, we're building a stripped-down field service management system called Field Service Lite. We'll use these tables:
The relationships are:
We're keeping the scope tight so you can focus on the app-building mechanics. A real deployment would add Account, Inventory, Parts Used, and SLA tables, but the patterns are identical regardless of scale.
Tip
Work inside a solution from the very first click. Go to make.powerapps.com, navigate to Solutions, and create a new solution tied to your publisher prefix (e.g., wsd_). Every table, form, view, and app you create during this lesson should live in that solution. This isn't optional ceremony — solutions are the unit of deployment, and building outside one means painful manual cleanup later.
In your solution, select New → Table. Configure the following:
Click Advanced options and pay attention to two settings:
Attachments — enable this if technicians will upload photos or documents to job records. You can't enable attachments after the fact without a schema change.
Auditing — enable at the table level if you need a history of who changed what. You'll also need to enable it at the environment level, but pre-checking the table setting is good hygiene.
After creating the table, add these columns:
| Column Name | Type | Details |
|---|---|---|
| Description | Multiline Text | Max length 2000 |
| Status | Choice | Values: New, Assigned, In Progress, Completed, Cancelled |
| Priority | Choice | Values: Low, Medium, High, Critical |
| Scheduled Date | Date Only | — |
| Completion Date | Date Only | — |
| Notes | Multiline Text | Max length 4000 |
For the Status column, use a local choice (specific to this table) rather than a global choice unless you plan to reuse the same values on multiple tables. Global choices look attractive but become a maintenance hazard when different tables need to evolve their status values independently.
Create a second table with these settings:
Add these columns:
| Column Name | Type | Details |
|---|---|---|
| Region | Choice | Values: North, South, East, West, Central |
| Certification Level | Choice | Values: Junior, Standard, Senior, Master |
| Availability | Choice | Values: Available, On Job, Unavailable, On Leave |
| — | ||
| Phone | Phone | — |
On the Service Job table, add two lookup columns:
servicejob_contact_customerservicejob_technician_assignedtechThe relationship name matters for API access and Power Automate flows — keep it readable. The framework generates an OData navigation property from this name, so servicejob_technician_assignedtech becomes _wsd_assignedtechnician_value in the underlying record structure.
Warning
When you create a lookup column, Dataverse automatically creates a 1:N relationship between the related table and this table. On the Technician table, this creates a related "Service Jobs" sub-grid relationship. By default, the relationship behavior is Referential, meaning you can delete a Technician even if they have open jobs. In a production system, consider changing the delete behavior to Restrict — which prevents deletion of a Technician record if related Service Job records exist — or Cascade if deleting a technician should cascade-delete their jobs. Change this in the relationship properties before you go live; changing it afterward requires careful data migration planning.
Forms are the primary way users create and edit records. Model-driven apps support four form types, and understanding when to use each saves you significant rework:
Navigate to your Service Job table → Forms → open the Main Form that Dataverse created by default (or create a new one).
The form designer presents a canvas divided into Header, Body (sections within tabs), and Footer. Here's how to think about each:
Header — always visible regardless of which tab is active. Put your most critical quick-read fields here. For Service Job: Status (as a read-only badge using a Choice column), Priority, and Assigned Technician. The header fields are rendered in the top strip of the form, visible even when users scroll through long tab content.
Body — organized into tabs and sections. Most users never notice tabs exist, so only create a second tab when the information is genuinely secondary and infrequent. For Service Job, use this structure:
Tab: General
Section: Job Details (2 columns)
Left: Job Number (read-only), Scheduled Date, Completion Date
Right: Customer (lookup), Assigned Technician (lookup), Priority
Section: Description (1 column, full width)
Description (multiline, 4 rows visible)
Tab: Notes & History
Section: Notes (1 column)
Notes (multiline, 6 rows visible)
Section: Timeline (1 column)
Timeline control (tracks emails, calls, tasks related to this job)
Footer — rarely used in modern apps. Skip it.
Don't overlook the Timeline control. It's a built-in composite control that surfaces Activities (emails, phone calls, tasks, appointments) and Notes related to a record in a unified chronological feed. Drop it into the "Notes & History" tab as a full-width section. This single control often justifies choosing model-driven over canvas for business users who live in their email — it surfaces communication context directly on the job record without any custom development.
To add Timeline: in the form designer, select your Notes section, then from the component library, drag Timeline into the section. Configure it to include Notes and the Activity types relevant to your business.
Simply placing a field on a form isn't enough — each field has properties that significantly affect the user experience:
Required vs. Business Required vs. Business Recommended: These are not the same. "Required" (set at the column level in Dataverse) enforces a database constraint — the record literally cannot be saved without a value. "Business Required" (set on the form field) prevents saving from this form but can be bypassed by API calls or other forms. "Business Recommended" shows a visual indicator without blocking save. For Status and Priority on Service Job: make them Business Required on the form. For Completion Date: leave it optional — you can't require a value that only applies to completed jobs without a business rule.
Read-only conditions: For Job Number, you want it read-only after creation. In the classic form designer, this requires a JavaScript web resource. In the modern form designer (the one that appears when you edit forms from make.powerapps.com), you can set field-level locking using Business Rules — the preferred approach for makers without developer skills.
Tip
The modern form designer (the one you get when editing forms from make.powerapps.com in 2024+) and the classic form designer (accessible via the legacy Customizations area in Settings) have different capabilities. The modern designer is the right choice for new development. Use the classic designer only when you need to add JavaScript web resources — the modern designer doesn't support that workflow.
On the Service Job form, you probably don't need a subgrid (it's the leaf entity in our model). But on the Technician main form, you absolutely want a subgrid showing that technician's Service Jobs.
Add a new tab called "Assigned Jobs" to the Technician main form. Insert a Subgrid component and configure it:
The subgrid renders as a mini-view inside the parent form. Users can open records from it, create new records (pre-populated with the parent lookup), and interact with the filtered view. This is the power of relational model-driven design — navigation between related records is inherent in the structure, not something you have to code.
The Quick Create form is what appears when someone clicks the global "+ New" button in the app header, or when they use the inline create option in a lookup. It should contain only the fields necessary to create a minimal valid record — not everything on the Main form.
For Service Job, configure Quick Create with:
That's five fields. The form saves quickly, the record opens in the Main form for full editing. Don't add Description and Notes to Quick Create — users who want those fields will use the full form.
The Quick View form for Technician will be embedded on the Service Job Main form, letting dispatchers see technician details (region, availability, certification level, phone) without navigating away from the job record.
Navigate to the Technician table → Forms → New Form → Quick View Form. Add:
Back on the Service Job Main form, find the Assigned Technician lookup field. Select it, and in the properties panel, add a Quick View Form using the Technician quick view you just created. Now when a dispatcher selects a technician on a job, they'll see that technician's current availability and region in a collapsible panel directly on the form — without opening a second tab.
Key insight
Quick View Forms pull data from the related record at load time. They don't refresh dynamically if the related record changes while the parent form is open. In a high-churn scenario (technician availability changes every few minutes), communicate this limitation to users — they may need to refresh the form to see current availability. A real-time solution would require a Canvas component embedded in the model-driven form, which is a significant architectural escalation.
Views are the list experience in model-driven apps — the grids users see when they navigate to a table. Understanding view types is critical because you have no control over which view a user sees first; that's determined by the default view settings and the context they navigate from.
Navigate to Service Job → Views → Create a new view called Active Service Jobs.
Configure the following columns (in this order — order matters for readability):
Set the filter criteria:
Set the default sort: Scheduled Date, ascending (soonest jobs first).
This is your primary operational view. Managers open the app, they see jobs that need attention, sorted by urgency.
A well-designed app has views for the main operational scenarios, not just one catch-all view. Create these additional views for Service Job:
My Open Jobs — filtered to Assigned Technician = Current User (using related field lookup) and Status not in [Completed, Cancelled]. This is the technician's personal dashboard view.
Filter:
Assigned Technician (Technician > System User > Related) equals [Current User]
AND Status not in [Completed, Cancelled]
Note the complexity here: Current User filters in model-driven views reference the System User record of the logged-in user. For this to work through a lookup chain (Service Job → Technician → System User), you need a relationship from Technician to System User. In our simple model, we didn't build that — we stored technicians as a custom table, not mapped to System Users. In a real deployment, you'd map Technician records to their corresponding System User via a lookup, enabling this pattern. This is why data modeling decisions made in the first hour determine what's possible in the app.
Critical Priority Jobs — filtered to Priority = Critical, Status not in [Completed, Cancelled]. Operations managers need this instantly accessible.
Jobs This Week — filtered to Scheduled Date this week (using the relative date filter "This Week"). Useful for scheduling views.
Completed Jobs Last 30 Days — filtered to Status = Completed and Completion Date = Last 30 Days (last X days filter). Important for management reporting.
The Quick Find view is the most neglected configuration in model-driven apps. Every time a user types in the search box at the top of a grid view, Dataverse queries only the columns defined in the Quick Find view's Find Columns — not the full table.
Navigate to Service Job → Views → Quick Find Active Service Jobs (this view already exists; you need to edit it).
Set the Find Columns to include:
Set the View Columns (what appears in search results) to:
Without configuring Find Columns, search will only match on the primary column (Job Number). Users who search for a customer name will get zero results even when matching jobs exist — and they'll file support tickets, and you'll feel bad about it.
Warning
Quick Find in model-driven apps performs a LIKE search (%searchterm%) on the specified columns. For large tables (100,000+ records), this can cause full table scans and significant performance degradation. For production tables expected to grow beyond 50,000 rows, consider indexing strategy in Dataverse and potentially restricting Quick Find columns to those with search indexes. The Job Number column (the primary column) is always indexed. Custom text columns are not indexed by default.
In the view designer, you can set column widths. This is more consequential than it sounds. The Unified Interface renders views in a responsive grid — on narrow viewports, columns collapse from right to left. Your most important columns should be leftmost, narrowest columns to the right.
Recommended widths for our Active Service Jobs view:
Total: 890px — fits comfortably on a 1280px screen with the navigation pane open.
Charts in model-driven apps are tied to views — they render the data from whatever view is currently active. Add a chart to the Service Job table:
Navigate to Service Job → Charts → New Chart.
Create a Jobs by Status bar chart:
This chart will render alongside the grid in the view panel, automatically filtering as the user switches views. When a manager switches to "Critical Priority Jobs," the chart instantly shows the status breakdown of just the critical jobs.
Create a second chart: Jobs by Assigned Technician
This surfaces workload imbalances immediately. If one technician has 12 open jobs and another has 2, a manager sees it in seconds.
The site map is the navigation structure of your app. It defines what appears in the left navigation pane, how it's organized, and what users can access. A poorly designed site map creates an app that technically works but feels confusing and unprofessional.
A site map has three levels:
For our app, design the site map as follows:
Area: Field Service Lite
Group: Operations
Subarea: Service Jobs → Table: Service Job, Default View: Active Service Jobs
Subarea: Technicians → Table: Technician, Default View: Available Technicians
Subarea: Customers → Table: Contact, Default View: Active Contacts
Group: Administration
Subarea: All Jobs (Admin View) → Table: Service Job, Default View: All Service Jobs
The Administration group is deliberately separate — you'll later apply security role filtering so that only administrators see it. More on that in a moment.
Open the App Designer. In make.powerapps.com, navigate to Apps → New App → Model-Driven.
Give the app a name: Field Service Lite. The system generates a unique name with your publisher prefix.
The App Designer presents a canvas with the site map editor on the left. Click Edit Site Map to open the site map designer.
Step 1: Name the Area. Click the default Area, change its title to "Field Service Lite" and its ID to area_fieldservice. The ID is used programmatically — keep it snake_case and descriptive.
Step 2: Create Groups. Add a Group called "Operations" (ID: group_operations). Add a second Group called "Administration" (ID: group_admin).
Step 3: Add Subareas. Under Operations, add three subareas:
For Service Jobs:
subarea_servicejobsFor Technicians:
subarea_techniciansFor Customers:
subarea_customersUnder Administration, add one subarea:
subarea_alljobs_adminStep 4: Set default views on subareas. Each subarea can be configured with a default view. Click a subarea and look for the "Default View" property. Set Service Jobs → Active Service Jobs, Technicians → Active Technicians (create this view on the Technician table first), Customers → Active Contacts.
Tip
Giving each subarea a specific default view rather than letting the table's default view win is a subtle but important UX decision. Users who navigate to "Service Jobs" should see Active Service Jobs — not "All Service Jobs" or whatever someone set as default on the table six months ago. The subarea-level default view override gives you explicit control without changing shared table defaults.
Step 5: Save and close the site map designer.
Back in the App Designer, you need to explicitly tell the app which tables it uses. This step surprises many first-timers — adding a table to the site map is not enough. You must also add the table to the app's entity list.
In the App Designer, find the Entities section (or Tables in newer UI). Add:
For each table, the App Designer will show you which forms, views, and charts exist. You can choose to include all components or specific ones.
Best practice: Include specific components, not "all." If you include all, then every future form or view someone creates on Service Job will automatically appear in your app. That sounds convenient until a developer adds a "Debug - Do Not Use" view and it shows up in your production app's view picker.
For our app, include:
This article focuses on the structural build, but you cannot consider a model-driven app "done" without at least a basic security posture. For a deep dive, see Power Apps Security: Roles, Sharing, and Data Permissions.
For Field Service Lite, you need two roles:
Field Service Technician — can read all Service Jobs, update Service Jobs where they are the assigned technician (row-level ownership), read Technicians and Contacts. Cannot delete anything. Cannot see the Administration group in the site map.
Field Service Dispatcher — can read and write all Service Jobs, all Technicians, all Contacts. Cannot delete. Can see the Administration group.
Field Service Administrator — full read/write/delete on all tables in scope. Sees everything.
Security roles in model-driven apps operate at two levels: table-level CRUD privileges and column-level security profiles. The site map visibility trick — hiding the Administration group from non-administrators — is achieved not through site map configuration but through security roles. If a user doesn't have read access to the underlying table or view, the subarea simply doesn't render. This is elegant and robust — there's no "hide this subarea if user is X" conditional logic you have to maintain.
Key insight
Model-driven apps respect security roles natively. You don't write If(User().Email = "admin@...", Navigate(...)) like you might in a canvas app. If a Technician's role doesn't grant read on the Contact table, they won't see the Customers subarea in the nav — and they won't be able to query Contacts from the Service Job form either. The security is enforced at the API layer, not in the UI.
In the App Designer, click Validate. The validator checks for:
Fix any validation errors before publishing. A common one: you added Service Job Quick Create to the entity list but forgot to include it — the validator will flag this because the "+ New" button won't work without it.
Click Publish. The publish operation compiles the app metadata, registers the site map, and makes the app available to licensed users in the environment.
Navigate to make.powerapps.com → Apps → find "Field Service Lite" → Play. You should see your site map, navigate to Service Jobs, see the Active Service Jobs view, and be able to create a new Service Job via Quick Create.
Now build a version of this app yourself, extending the scenario slightly. Complete the following tasks:
Task 1: Add a "Parts Used" table
Create a new table called Part with columns: Part Name (primary), Part Number (text), Unit Cost (currency), Stock Quantity (whole number). Create a many-to-many relationship between Part and Service Job (a job can use many parts; a part can appear on many jobs). Add a subgrid to the Service Job Main form showing parts used on that job.
Task 2: Create a Technician Availability view
On the Technician table, create a view called Available Technicians with filter: Availability = "Available". Include columns: Full Name, Region, Certification Level, Phone. Set this as the default view on the Technicians subarea.
Task 3: Build a Quick View Form for Customer
Create a Quick View form for Contact showing: Full Name, Email, Phone, Company Name (Account). Embed it on the Service Job Main form on the Customer lookup field.
Task 4: Configure the Quick Find view for Technician
Edit the Technician Quick Find view to search on: Full Name, Email, Phone. Add Region and Certification Level to view columns.
Task 5: Add a dashboard
Create a model-driven dashboard called "Dispatcher Overview" with four tiles:
Add this dashboard as a subarea in the Operations group of the site map (Type: Dashboard rather than Entity).
When you're done, validate and publish. Share the app with a test user who has only the Field Service Technician role and verify they cannot see the Administration group, cannot delete records, and cannot navigate to the All Jobs admin view.
If you create tables, forms, or views from the Tables area (not inside a solution), they get added to the Default Solution — a catch-all container that can't be cleanly exported. Move everything into a named solution from the start. If you've already built outside a solution, use the "Add existing" option to pull components in, but audit carefully — you may be missing dependent components (like global choices or relationships) that didn't get auto-included.
If you delete a field from a form that's referenced in a business rule, the business rule silently breaks — the rule still exists but the field condition it checks no longer has a UI element, and behavior becomes unpredictable. Before removing any field from a form, use the Solution dependency checker (in the solution, click "Show dependencies" on the form component) to identify what references it.
Covered above, but worth repeating. Users search and find nothing, think the app is broken, contact IT, waste everyone's time. Takes five minutes to fix before launch; causes hours of support pain after.
A field marked Business Required on the form will block save in the UI. But a Power Automate flow writing directly to the table doesn't use the form — it bypasses form validation entirely. If a field is truly required for data integrity, set it as Required at the column level in Dataverse. If it's only required in the context of the app workflow, Business Required on the form is correct.
Users don't explore model-driven forms like websites. They navigate to a record, read the default tab, edit what they need, save. If critical information is hidden on a second tab with an innocuous label, users won't find it. Design with one tab unless you have a compelling reason for two. "The form is too long" is not a compelling reason — use sections to organize long forms.
When you include all forms/views/charts for a table, newly created components immediately appear in your app without any review. In a production environment with multiple developers, this causes unexpected form and view appearances. Always manage inclusion explicitly.
If you create a view with filter criteria and the deployed view shows records that should be filtered out, check two things: First, confirm you published after saving the view (views require explicit publish to take effect in the app). Second, check whether the view is being overridden at the subarea level — if the subarea doesn't explicitly set a default view, it may fall back to the table's default view, not the one you intended.
If clicking the global "+ New" button opens the main form instead of the quick create form, the Quick Create form isn't included in the app's entity configuration. Go to App Designer → select the table → verify Quick Create form is checked in the forms list. Also verify that "Enable quick create" is enabled on the Quick Create form itself (form properties → Enable Quick Create Form).
If a subgrid on a parent record shows all records from the related table rather than just those related to the current record, check the subgrid configuration. The "Show Related Records" property must be enabled, and the relationship it uses must be the correct 1:N relationship. If you have multiple relationships between the same two tables, the subgrid may be using the wrong one.
Warning
Subgrid views are not the same as table views. When you configure a subgrid, you specify a view to use for its column layout — but the filter criteria of that view is overridden by the subgrid's relationship filter. Don't put relationship-specific filters in the subgrid's base view (like "Assigned Technician = X") expecting them to apply — they won't. The relationship filter always wins. Put only column selection and sorting logic in the subgrid's view.
You can create multiple Main Forms for a table and assign each to different security roles. A Technician sees a simplified form with read-only fields; a Dispatcher sees the full editing form. This sounds elegant but creates a significant maintenance burden — every schema change (new column, field label change) must be applied to each form separately.
A better pattern for most scenarios: one Main Form with field-level visibility controlled by business rules. Business rules can show/hide fields, enable/disable fields, and set values based on column conditions, role, and form context. This keeps the form definition unified while still adapting to user context.
Reserve multiple Main Forms for genuinely different workflows — for example, a job that a Technician fills out in the field has fundamentally different fields than a job that a Billing user reviews after completion.
As your model-driven app grows, you'll face a choice: add more areas to your existing site map, or deploy separate apps for different user groups.
Multiple apps in the same environment share the same underlying tables and data — the difference is which tables, views, and forms each app surfaces. A separate "Technician App" can show only Service Jobs and use only mobile-optimized views, while the "Dispatcher App" shows all tables with full management views. Users log in to different apps from their app list.
The trade-off: multiple apps are easier to manage from a security and UX perspective, but create versioning overhead (publish changes to each app separately) and potential confusion when users have access to multiple apps. For Field Service Lite, one app with two areas is correct. At 10+ tables and 3+ distinct user groups with fundamentally different workflows, separate apps make more sense.
Model-driven app views query Dataverse directly. For tables with millions of rows, consider:
You've built the complete structural skeleton of a production-worthy model-driven app. Let's recap what you've accomplished:
Tables and relationships — you created the Service Job and Technician tables with appropriate columns and relationship types, making schema decisions (delete behavior, data types, choice scope) that affect long-term maintainability.
Forms — you built a Main form with tabs, sections, timeline, subgrid, and Quick View form integration. You configured Quick Create for fast data entry. You understand the difference between Business Required and column-level Required, and why that matters for API-driven writes.
Views — you built operational views (Active Service Jobs, My Open Jobs, Critical Priority Jobs), configured Quick Find correctly, and added charts for visual data analysis.
Site map — you designed a two-group navigation structure with security-driven visibility, explicitly managing component inclusion to avoid unreviewed changes reaching production.
Publishing and validation — you understand the publish workflow and the common validation failures that prevent clean deployment.
Where to go next:
The natural next lesson in this path is Business Rules and Business Process Flows — using model-driven app's no-code logic layer to enforce conditional validation, auto-populate fields based on status changes, and guide users through multi-stage processes like a technician accepting a job, completing it, and logging outcomes.
After that, you'll want to explore role-based security at depth — the article on Power Apps Security: Roles, Sharing, and Data Permissions covers column-level security profiles, row-level ownership, and the difference between Organization, Business Unit, and User/Team scoping.
If you're coming from a canvas background and want to understand how model-driven forms compare to canvas form patterns, Power Apps Controls: Galleries, Forms, and Data Tables - Advanced Architecture and Performance makes the contrast explicit.
Model-driven apps reward structural thinking. Every hour you invest in getting the data model and component configuration right pays back in an app that scales, adapts to new requirements, and stays maintainable without heroic refactoring. You've built the foundation — now build on it.