Model-Driven App Definition
Schema id: model-driven-app@1
Machine-readable schema: model-driven-app.schema.json
The canonical representation of a Model-Driven App (Section 6.7) — a named app that groups a curated subset of the environment's tables, dashboards and reports into a navigation (site map) with a home page, the way a Power Apps model-driven app does. Validated identically whether it comes from the designer, an API call, an AI assistant, or a Solution import.
Shape
| Field | Type | Required | Notes |
|---|---|---|---|
logicalName | string | yes | Publisher-prefixed, lower snake-case (for example, par_sales_app). |
displayName | string | yes | Human-readable app name shown on the launcher tile. |
publisherPrefix | string | yes | The owning publisher's prefix; the logical name must start with it. |
description | string | no | Short description shown on the launcher tile. |
icon | string | no | Icon key for the launcher tile and the app's rail header. |
supportsTasks | boolean | no | Whether this app takes part in business processes. With it on, the Shell offers the task list and the Decide button while the app is open; with it off it draws neither. Off is the default, because most apps are not part of a process and an inbox on one of those is chrome that never has anything in it. |
home | destination | yes | What opens at /apps/{id}; must appear in navigation. See below. |
navigation | array | yes | One or more navigation groups (the site map; see below). |
includes | array | no | The app's components: things it can open that are not on its site map. See below. |
Destination
Somewhere the app can go — one shape, used for a navigation item and for home. That is
deliberate rather than incidental: "home must be somewhere the navigation goes" is then a
comparison of two values of the same type, instead of three near-identical checks that could
drift apart one kind at a time.
| Field | Type | Required | Notes |
|---|---|---|---|
type | Entity | Dashboard | Report | yes | The three things the Player can put on a screen: a grid, a dashboard host, a report viewer. |
target | string | yes | The destination's logical name. Which store it must exist in is decided by type. |
Navigation group
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | Group heading shown in the app's left navigation. |
items | array | yes | One or more navigation items (see below). |
Navigation item
A destination, plus how this app wants it labelled. The label and icon overrides apply to every kind — an app that calls the Accounts table "Customers" has the same reason to call a dashboard "Today".
| Field | Type | Required | Notes |
|---|---|---|---|
type | Entity | Dashboard | Report | yes | What this item opens. |
target | string | yes | Its logical name (must exist in the environment, as that kind of thing). |
displayName | string | no | Label override for this app's rail; the component's own display name is used if omitted. |
icon | string | no | Icon key for the rail item. |
forms | array of string | no | Entity items only. The forms of this table this app offers, by logical name. See below. |
views | array of string | no | Entity items only. The views of this table this app offers, by logical name. |
Every destination referenced — in a navigation item and in home — must exist in the
environment in the store its own kind names, and home must match one of the navigation
items by both type and target. Both are enforced at save time in addition to schema
validation (modelapps.app.unknown_entity, modelapps.app.unknown_dashboard,
modelapps.app.unknown_report, modelapps.app.home_not_in_navigation).
forms and views on a Dashboard or Report item are refused, not ignored
(modelapps.app.component_selection_not_allowed): neither has forms or views, so a selection
there is an authoring mistake that would otherwise vanish silently and leave its author
believing they had narrowed something.
Who may open one
Opening a dashboard and running a report are grants on that component — permissions named
<logicalName>.Open and <logicalName>.Run, held by a security role like any other, with
*.Open / *.Run as the wildcards that keep covering whatever is built next. Placing one in
a site map does not grant it. What the dashboard's widgets and the report's datasets then read
is authorized in its own right regardless, so a user who reaches a dashboard they have no data
for sees empty widgets rather than somebody else's numbers.
Component selection — forms and views
A table accumulates forms and views over its life: an admin form beside the one a salesperson uses, a "My open items" view that matters to one team. Every app showing every one of them is how a clean table becomes a cluttered app, so a navigation item may name the ones its app offers.
Absence and an empty array are not the same thing, and neither is "everything ticked".
| Value | Meaning |
|---|---|
| key absent | Every form/view the entity has, including ones added later. The default for a table just placed in an app, and what every app authored before this existed means. |
| a list | A closed choice. A form added to the entity afterwards does not appear in this app until it is listed. |
[] | Invalid. It cannot mean "all" (absence does) and must not mean "none" — a table with no form cannot be opened. |
Each listed name must be a form/view of that entity, checked at save time
(modelapps.app.unknown_component). A mistyped name is worth failing over rather than
ignoring: it would otherwise silently narrow the app to fewer forms than the author chose,
and the app would look correct until a user met a table they could not open.
The list decides membership only, never order — the runtime keeps the entity's own ordering and its own default, where that default survives the selection. Ordering a table's views within an app is a separate capability and is not this one.
Included page — includes
The navigation says what an app shows; includes says what it contains, and an app contains
more than it shows. A process opens a form of a table nobody browses; a designed command opens a
dashboard deliberately kept off the rail; a report is reached from a record and never from the
navigation. Without this list every such destination would be unreachable, or every app would have
to show everything it might ever open.
| Field | Type | Required | Notes |
|---|---|---|---|
type | Entity | Dashboard | Report | StudioApp | yes | What kind of thing this is. |
target | string | yes | Its logical name. |
forms | array | null | no | Tables only. The forms of this table the app may open, by logical name — the same rule as a navigation item's. |
views | array | null | no | Tables only. The views of this table the app may open. Same rule as forms. |
forms and views follow the rule in Component selection
exactly: absent means every one, a list is a closed choice, and an empty array is invalid.
Example
{
"logicalName": "par_sales_app",
"displayName": "Sales",
"publisherPrefix": "par",
"description": "Manage accounts and invoices",
"icon": "briefcase",
"home": { "type": "Dashboard", "target": "par_sales_overview" },
"navigation": [
{
"title": "Sales",
"items": [
{ "type": "Dashboard", "target": "par_sales_overview", "displayName": "Overview", "icon": "gauge" },
{ "type": "Entity", "target": "par_account", "displayName": "Accounts", "icon": "users" },
{
"type": "Entity",
"target": "par_invoice",
"forms": ["par_invoice_main"],
"views": ["par_invoice_active", "par_invoice_overdue"]
},
{ "type": "Entity", "target": "par_contact" },
{ "type": "Report", "target": "par_invoice_print", "displayName": "Print invoices" }
]
}
]
}
This app opens on its dashboard rather than on a grid of rows, which is what a home
that is not a table is for. par_invoice is curated — the app offers one of its forms and
two of its views, and a view added to the table tomorrow will not appear here.
par_account and par_contact name no selection, so the app offers everything those tables
have, now and later.