Model-Driven Form Definition
Schema id: model-driven-form@2
Superseded version: model-driven-form@1
(model-driven-form-v1.schema.json) stays
published and registered so an already-deployed Solution is still validated against the
schema it was authored to. Version 2 only adds the optional controlProps.lookupView,
so every v1 definition is also a valid v2 one.
Machine-readable schema: model-driven-form.schema.json
The canonical representation of a Model-Driven Apps form (Section 6.7) — the layout used to view and edit a single record. 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. |
displayName | string | yes | Human-readable form name. |
entity | string | yes | The owning entity's logical name. |
description | string | no | The line shown under the title on the form's title bar. |
icon | string | no | The icon shown on the form's title bar. |
sections | array | one of | One or more titled sections — the legacy flat layout (see below). |
tabs | array | one of | The richer tabs layout (see below). |
actions | array | no | Designable command-bar actions. |
A form carries either sections or tabs, not neither. New definitions should use
tabs; sections is accepted so existing forms keep working, and is treated as one
tab of single-column rows.
Section (legacy sections layout)
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | yes | Section heading. |
columns | integer | yes | Layout columns, 1–4. |
fields | array | yes | Field placements (see below). |
Tab (tabs layout)
| Field | Type | Required | Notes |
|---|---|---|---|
id | string | no | Stable designer id. |
title | string | yes | Tab title. |
icon | string | no | Icon shown next to the tab title. |
rows | array | yes | Full-width layout rows: { id?, columns: 1–4, sections: [...] }, each section { id?, title, icon?, span?, fields, timeline? }. |
Field placement
| Field | Type | Required | Notes |
|---|---|---|---|
field | string | see notes | The field's logical name (must be defined on the entity). Omitted only for a Label placement, which is static content and binds to no field. |
label | string | no | Label override; the field's display name is used if omitted. |
control | string | no | A Custom Control logical name mounted in place of the native field editor. The control must be installed in the environment; if it is not, the field falls back to its native editor. |
controlType | string | no | The built-in control to render the field with (Section 6.13.5). One of Text, TextArea, Check, Toggle, Date, DateTime, Time, PersianDate, PersianDateTime, PersianTime, Lookup, Dropdown, Label. Omit to let the Player pick the default input for the field's data type. The Persian… variants render the same picker as their Gregorian counterparts but open on the Jalali calendar whatever the app language; the stored value is still a Gregorian instant. Label is static Markdown content (controlProps.labelText) with no bound field. |
required | boolean | no | Whether the field is business-required on this form (Section 6.13.5) — the user must fill it before saving. A form concern, not a database constraint (every entity column is nullable); the Player enforces it in the browser and the server does not. Absent defers to the field's own IsRequired. |
readOnly | boolean | no | Whether the field is shown read-only on this form (display, not edit). A read-only field renders as plain text with no input control. Absent is not false: absent means editable on a tab but read-only in header, so a header field is made editable with an explicit false. |
controlProps | object | no | The chosen control's properties (see below). |
Control precedence: a Custom control wins over controlType, which wins over the
field data type's default. Omit both and the Player picks the default.
Required is a form rule, not a column constraint. required lives on the placement,
so the same field can be required on one form and optional on another, and it is checked
only in the Player when the user saves. Entity columns are always nullable at the database
level (Power-Apps-style), so a record created by an import or API call is never rejected
for a missing required field — the one value the server insists on is the entity's
primary (name) field.
Control properties (controlProps)
| Field | Type | Read by | Notes |
|---|---|---|---|
dateFormat | string | Date, DateTime, Time (and their Persian… variants) | Display format. Tokens: yyyy/yy, MMMM (full month name)/MMM (short)/MM/M, dd/d, HH, mm, ss; any other characters are literal. Absent uses the calendar's default — d MMMM yyyy for Persian (e.g. 12 اردیبهشت 1405), yyyy/MM/dd for Gregorian. |
trueLabel | string | Check, Toggle | The label shown for the "yes" state. |
falseLabel | string | Check, Toggle | The label shown for the "no" state. |
rows | integer | TextArea | Visible row count, 2–20. |
lookupEntity | string | Lookup | Logical name of the entity the lookup resolves against. |
lookupView | string | Lookup | Logical name of the view supplying the columns shown in the lookup's results panel. Absent lets the platform resolve one — see the view schema's viewType. |
lookupRowCount | integer | Lookup | How many candidate rows the lookup's panel offers at once (1–500). Absent uses the platform default of 10. The server clamps whatever is asked for — a picker is not a report. Added after v2 by widening in place: every definition valid without it is still valid. |
lookupPrimaryField | string | Lookup | Per-control override for which field is each candidate's name line. Absent uses the target table's own primary field; a name that is not a field on the target is ignored. |
lookupSecondaryField | string | Lookup | Per-control override for the second line under the name, overriding the field the target table nominates. Absent uses the table's nomination. |
lookupImageField | string | Lookup | Per-control override for which File/Image field is each candidate's picture, overriding the target table's record image. Absent uses the table's image; a non-image field is ignored. |
imageSize | string | Image field | How large an Image field's preview renders — sm (default, avatar-sized), md, or lg (a prominent picture). Only meaningful for an Image field. |
labelText | string | Label | A Label control's fixed text, rendered as a small safe Markdown subset: #/##/### headings, **bold**, *italic*, `code`, - lists, [text](url) links (http/https/mailto/relative only), and {color:name}…{/color} (an allow-listed colour name or #hex). No raw HTML is ever rendered. |
options | array | Dropdown | Fixed options, each { value, label }, belonging to this one field. Prefer an Option Set for anything reused across fields or tables: a set is a shared, versioned component with collision-proof values, whereas these options are a private list on one form. A field whose data type is OptionSet ignores this and draws from its set. |
Properties that do not apply to the chosen controlType are ignored, not rejected —
so switching a field's control and switching back does not lose its configuration.
Every field referenced must be a field defined on the entity — this is enforced at
save time in addition to schema validation.
Timeline section (timeline)
A section in a tab may be a timeline instead of a group of fields: the records of other
tables whose lookup points at the open record, merged and newest first — a customer's emails,
calls and notes in one column. A section is one or the other: when timeline is present,
fields must be an empty array.
| Field | Type | Required | Notes |
|---|---|---|---|
sources | array (1–10) | yes | The tables it reads, one entry each (below). |
pageSize | integer (5–100) | no | Items shown before Load more; 20 when absent. |
Each source:
| Field | Type | Required | Notes |
|---|---|---|---|
entity | string | yes | The source table's logical name. |
regarding | string | yes | A lookup on the source table that targets the form's table, or a multi-table lookup that lists it. The timeline shows the rows whose value is the open record. |
date | string | yes | A Date and Time field of the source (system fields such as created_at_utc included). Items are ordered by it, newest first. |
title | string | yes | The field each item is headed with. |
body | string | no | A field whose opening words each item shows. |
type | string | no | An option-set field that tells the source's rows apart. Each item takes its icon and colour from its option, the filter shows one chip per option, and New offers one entry per option, pre-filled. |
status | string | no | A field shown beside each item's date. |
On save and on import, every name is checked against the environment's tables: the source
table must exist, regarding must be able to point at the form's table, date must be a
date, type must be a Choice, and the rest must exist. A timeline that fails any of these is
refused (modelapps.form.timeline_*) rather than drawn empty. Deleting a field a timeline
uses removes it from the timeline: an optional part is dropped, and a source that loses its
regarding, date or title is removed.
Example
{
"logicalName": "par_invoice_main",
"displayName": "Main",
"entity": "par_invoice",
"description": "The invoice's main form",
"icon": "Receipt",
"tabs": [
{
"id": "tab-general",
"title": "General",
"rows": [
{
"id": "row-1",
"columns": 2,
"sections": [
{
"id": "sec-header",
"title": "Header",
"fields": [
{ "field": "par_name", "required": true },
{ "field": "par_total", "label": "Amount" },
{ "field": "par_number", "readOnly": true },
{
"field": "par_due",
"controlType": "Date",
"controlProps": { "dateFormat": "dd/MM/yyyy" }
},
{
"field": "par_paid",
"controlType": "Toggle",
"controlProps": { "trueLabel": "Paid", "falseLabel": "Unpaid" }
},
{ "field": "par_signature", "control": "par_signature_pad" }
]
}
]
}
]
}
]
}