- name
- app-builder
- version
- 1.0.0
- description
- Builds and edits a model-driven Power Apps app from a natural-language intent — tables, columns, relationships, adaptive forms with sub-grids, views, Choice-column charts, business rules, business process flows, generative page intents for overview/dashboard surfaces, and an app module + sitemap. Runs an interactive authoring flow with plan approval and can download a deployed app back into an editable spec to change it. Use when the user says "build an app for X", "create a model-driven app", "make me an app to manage Y", "add a business process flow", or "edit/add to my app". Do not use it for a generic web or React app or website, Power Pages, canvas apps, or Power Automate, or when the user rules out Power Platform, Dataverse, or model-driven apps (for example "no Power Platform"). This skill stands alone and does not require /genpage — but for a standalone generative page added to an app that already exists, use /genpage instead.
- author
- Microsoft Corporation
- argument-hint
- <app description>
- user-invocable
- true
- allowed-tools
- Read, Write, Edit, Bash, Glob, Grep, Task, AskUserQuestion, EnterPlanMode, ExitPlanMode, TaskCreate, TaskUpdate, TaskList, read, edit, execute, search, agent, todo
> **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.
> **Model check (lightweight models only)**: If you are a lightweight model optimized for fast, simple tasks, tell the user in your first message that `/app-builder` works best on a general-purpose coding model or an advanced reasoning model — designing the App Spec, reasoning about the dependencies between tables, relationships and artifacts, orchestrating the build and reconciling what already exists take multistep planning, tool use and validation — and recommend switching; then continue. On any other model, skip this note and say nothing about it.
# app-builder — intent → model-driven app
Turn a natural-language intent into a deployed model-driven app. You author a reviewable **App Spec**
(JSON) with the user across confirmed turns, then a deterministic engine (`cds-maker-sdk`, vendored)
builds it — tables/columns/relationships, sample data, views, Choice-column charts, adaptive forms
with sub-grids, **generative pages** for overview/dashboard surfaces, and the app module + sitemap.
The **same spec drives create and edit**: download a deployed app back into a spec, change it, and
re-run the build (it's idempotent).
## CRITICAL — run the interactive flow in THIS conversation (the main loop)
> **You MUST run the authoring questions and the build narration yourself, in the main
> conversation. Do NOT dispatch a subagent (`Task`) for the interactive steps.**
>
> A subagent is headless — `AskUserQuestion` and plan mode do not reach the user from inside one
> (its only output is its final message). The whole point of this skill is the multi-turn,
> propose-then-confirm experience, so every `AskUserQuestion`, `EnterPlanMode`, and live build
> status line must originate here, in the main loop.
## CRITICAL — the user sees your chat message, NOT tool output
> **Shell/tool output — the result of running `preview-app.js`, a dry-run plan, or a lint —
> is COLLAPSED BY DEFAULT in the UI. The user does NOT see it unless they manually expand the
> tool panel.** Running the command is therefore NOT the same as showing the user. Every artifact
> the user must **read, review, or approve** — the whole-app **preview wireframes**, the
> **dry-run build plan**, and blocking **lint findings** — MUST be reproduced **verbatim in your
> chat reply**, inside a fenced ` ``` ` code block. Never say "the preview looks right" and leave
> the content buried in a collapsed panel: **paste it into your message.** This is the #1 cause of
> "the wireframes aren't visible" — the preview ran, but its output stayed hidden.
## Capabilities — the full toolbox (pick best-fit per requirement)
You are a **complete** model-driven app builder, not a single-surface tool. Everything below ships in
one App Spec and one build — choose what best serves the user's requirement to make a **useful,
prod-ready** app; don't under-build (a bare table list) or over-build (surfaces nobody asked for):
- **Data model** — tables (give each custom table a **meaningful Fluent-style SVG table icon by default**; propose what the glyph will **depict** in words — never a Fluent token name — and record it as `iconDescription` before drawing the SVG — see [`references/authoring-flow.md`](../../references/authoring-flow.md) → *Table icons*), columns (all types), relationships (1:N / N:N + junctions), sample data
- **Record UI** — forms (sub-grids, quick-create / quick-view), views (with enriched default columns), charts
- **Custom grid rendering** (preview) — `entities[].columns[].visualization`: render a column as a
`RadialDial`, `LineChart`, `HeatMap` or `StarRating` in **every** grid and view that shows it,
instead of plain text. Reach for it when a column is a *magnitude a user scans* (a score, a
utilization %, a rating, a priority) rather than a value they read exactly — it makes a list
scannable at a glance for one line of spec. It is per-*column*, so set it once on the column, not
on each view. **Preview:** on an environment where it is not provisioned the build skips it and
everything else still deploys, so it is always safe to include.
- **Actions** — modern command-bar buttons (incl. flyout / split menus), web resources (form JS / HTML / CSS).
Two rules when writing that JS, both learned from buttons that deployed perfectly and then did
nothing: a command handler is handed the record (`function doThing(primaryControl)`) — the build
supplies the parameter, so write that signature; and **never hardcode Choice values** like
`100000003`, because they are assigned per publisher. Resolve by label via `getOptions()`
(see `references/app-spec-schema-advanced.md` → webResources). Note also that command and web-resource
**edits do not redeploy on rebuild** — the phases reuse what exists, so changing a button or a
script means deleting it first (the build warns, and verify fails, when a web resource's deployed
content differs from the spec's). This does not apply to recovery of the build's own
`WEB_RESOURCE_UNVERIFIED` row: keep that row and the same build workspace, then follow its halt.
When the maker wants a rebuild to keep a web resource's content in step with the spec, mark that
resource `reconcile: "declared"` instead (`minimumPluginVersion: "2.21.0"`) — only when asked, one
resource at a time, never a shared (`external`) one, and never every resource of a downloaded or
older spec. A rebuild that would change one that exists stops before writing and names its approval
(`--authorize adopt:webresource:<web resource name>`) with the components that depend on it: show them to the
maker and let them choose. Emptying its content also needs `--allow-destructive`; the halt says so.
- **Form logic without code** — `businessRules[]`: show/hide, lock/unlock, set-required and
set-value, driven by a condition on the record. **Reach for a business rule before form JS** when
the requirement is field-level and declarative — it is visible in the maker, survives solution
export, and needs no web resource. Use form JS when the logic needs a real API call, cross-record
work, or anything beyond the four supported actions.
- **Guided processes** — `businessProcessFlows[]`: the staged bar across the top of a record
(ordered stages, each with steps bound to that table's columns — **every step must bind a `field`**;
the platform rejects one without, so use a Boolean flag for a manual check-off). Reach for one when
the user describes work moving through **phases** — "triage →
investigate → resolve", "lead → qualify → close". Author it `Active` (the default) or the stage bar
does not appear at all. Who may run a flow is `securityRoles: { "personas": [...] }` (Active flows
only). v1 is single-entity and linear: cross-entity stages, branching and stage actions are
**rejected** by the spec gate, so offer Maker for those rather than writing them into the spec.
- **Surfaces** — **generative pages** (modern dashboards / overviews / analytics / landing — the default),
classic dashboards (opt-in), external URLs
- **App shell** — the app module + sitemap, with per-subarea icons. Turn on the **modern shell** with
`app.newLook: true` unless the user asks for the classic one; it is opt-in and best-effort, so a
tenant without the setting still gets a working app. `app.headerNavigationRefresh` controls the
**Wave 2 header/navigation refresh** — a *separate, independent* setting whose platform default is
**ON**, so set it to `false` only when the user explicitly wants the classic header.
- **App theme and header logo (Preview, 2.21.0)** — `app.theme`. Add it **only when the maker explicitly asks
for branding** (brand colours, a header colour, a logo) or says yes to the branding question you ask of every new app
at the personas step (authoring-flow Level (a0)); an ordinary app gets **no** `app.theme` without one of those, and you never
invent a palette, a font or a logo. Offer the choices: a `basePaletteColor` (or explicit `palette` slots), the
`appHeader` colours, a `font` only if they name one, and a `logo` that is an **existing image web resource**
(or one you also declare in `webResources[]`), never a URL. Say before the build that the theme is published
with the **app**, and that an app publication publishes **every** pending change of the app, not only the theme.
The theme does not change the `newLook` / header choices above — do not switch them to fit a theme. Omit the key
to leave the app's theme alone; `null` removes the app's override (the environment theme applies again). Theme
versions are never deleted, and a logo the theme only references is never touched by teardown. `--verify` reports the theme's **API state** (saved,
pending publish, live) — tell the maker to check the header in the app itself.
- **App membership** — which forms, views and charts the app offers (`app.mainForms`, `app.tables`).
**Only these forms on an existing app.** `app.mainForms` adds the forms you list. To also take the table's other Main
forms **out of the app** on an update, set `"mainFormsOnUpdate": "exact"` — ask the maker first, it is a removal from the
app's membership (no form is deleted). It needs an unmanaged, published app; the build publishes only once Dataverse reads
back exactly your list, and otherwise stops with the change saved and the app's publication owed.
**Pin the artifacts the spec declares.** `"pinDeclaredArtifacts": true` adds the spec's own forms, views, charts and
dashboards to the app explicitly — use it for a chart or view of a table that is not in the navigation. It only adds.
When to use them: `"exact"` only on an explicit "only these forms" / "remove the other forms" ask, after
showing the maker which tables it covers (the build refuses it on an existing app without
`--allow-destructive`); never for "add the Summary form", which `app.mainForms` alone does. Suggest
`pinDeclaredArtifacts: true` for a **new** app whose spec declares a chart, view or dashboard of a table
that is not in its navigation. Never turn either on by yourself for an existing app. Both need
`minimumPluginVersion: "2.21.0"`.
- **Security & access** — one **security role per persona**, sized from that persona's jobs-to-be-done
(the entity access each job needs, unioned into the role), so the app **opens for non-admins**.
- **Confidential columns** — `isSecured` + `fieldSecurityProfiles[]`. Reach for them when the design
names a value only some people may see or change (salary, medical notes, an identifier, private
comments): a security role cannot hide one column, a profile can. Name who gets each profile
(`assignTo` users or teams, by id) — never infer them from a persona — and say in the preview who
gains access, or that no assignment was requested. Tell the maker that System Administrators see
every secured column (test as a non-admin), that an open app may need a reload, to grant `read`
unless `update` is needed, that unsecuring a column deletes its permissions
(`--authorize isSecured:<table>.<column>`), and that a teardown keeps each declared profile and names it.
- **Portable schema** — `{ "value", "label" }` options when an integration or another environment
depends on the option numbers; `dateTimeBehavior: "DateOnly"` for birthdays, due and renewal
dates, `"TimeZoneIndependent"` for a time that means the same clock everywhere (omit it for
timestamps); table `searchEnabled` / `duplicateDetectionEnabled` only when the maker asks. Never
change an existing column's date behaviour or turn duplicate detection off unasked: both need
`--authorize`. All need `minimumPluginVersion: "2.21.0"` — see
[`references/app-spec-schema-advanced.md`](../../references/app-spec-schema-advanced.md).
- **Declared views, charts and search columns** (`minimumPluginVersion: "2.21.0"`) — write
`reconcile: "declared"` on each view or chart you author for a **new** app, and, when editing, on the
one existing view or chart the maker asks to change: a rebuild then applies its columns, filters, sort
and joins (`linkEntities`) instead of only adding columns. Never set `title` on a new declared chart,
and give a declared view its own name — not the stock `Active <plural>` or `Inactive <plural>`, which
every table already has, so changing one is an adoption. Two declared views on one table need different
names, and so do two declared charts: a `viewId` pin does not tell them apart, and the build refuses the
pair (see Approvals in the advanced reference).
For a new table you create, propose `entities[].quickFind.findColumns` (the primary name plus what a
user types in the search box: a number, an email, a code) and confirm it with the maker; on an
existing table, only when the maker names what search should match. A change to a view or chart that
already exists stops the build with nothing written and names its approval
(`--authorize adopt:view:<table>/<name>` or `adopt:chart:`, `overwrite:` for changes made elsewhere, `--allow-destructive`
to remove columns): explain it and let the maker choose; never edit the spec to avoid it. A local copy
that differs from the server's stops it the same way (`workspace-copy-unsent-edits`) and is kept: ask
whether to put that change into the spec and refresh the local copy, or approve the `overwrite:` key
it prints (`overwrite:quickfind:<table>` for search columns); pass a key exactly as printed. Never write
`reconcile` on a web resource except as the web-resource bullet above says, on a form, a dashboard, a business
rule or a business process flow except as their bullets below say, and never write a command `key` (keyed command bars are refused
in this version). Leave a downloaded or older spec without these fields as it is — see the advanced reference.
- **Declared form layouts** (`minimumPluginVersion: "2.21.0"`) — write `reconcile: "declared"` on a form
only when the maker asked its layout to follow the spec (fields, spans, sections, field options, sub-grids,
handlers) or said yes when you offered it at Level (b); not for "add a form" or "add a field" with no layout. Tell the maker: controls the spec does not
declare stay where they are and nothing is deleted (so no `--allow-destructive` is needed for them); a layout the build cannot place safely stops before any
write; a table publish puts live every saved change on that table, not only this form, with or without
`--publish` (a declared dashboard is published after its write either way); and Dataverse does
not provide a working conditional-write guard for a form's saved layer, so the build re-checks it just
before writing, but an edit made in Maker at that instant cannot be detected. Never write `genPages`, `prune`, `isDefault`,
`deactivateOtherMainForms`, `securityRoles` or a `mainFormOrder` place on a declared form (refused).
An existing form is bound only with `--authorize "adopt:form:<table>/<formtype>/<name>"` (lower case;
`overwrite:form:` for changes made elsewhere): explain it and let the maker choose. `--verify` checks a
declared form against the spec, sub-grid views included (a form whose publish is unconfirmed is unverified,
not passed). A download brings declared forms back only with `--include-managed --managed-manifest <store
file>` (one with an event handler comes back with its handler library as an `external` web resource when
that library is this publisher's own unmanaged script; otherwise it is left out and named in
`managedForms.stale`, and its declaration is written ready to paste in the new file `managedFragments.file` names (`.maker-workspace/managed-fragments-<time>-<random>.json`)
with the library it needs, so tell the maker to add both from the spec it was built from); it carries no binding, so the first build that binds each form from the downloaded folder needs
`adopt:form:<key>` (a rebuild that writes nothing may leave it unbound). A form removed from the spec and
then deleted in Maker is released by the next build, which names it (`managed-root-retired`); until then
`--verify` fails it. One whose publish is unconfirmed is not released (see the unconfirmed publish below). A
build that warns a form's or dashboard's retirement could not be confirmed stops before it writes anything
(`managed-not-settled`) and sends nothing more in that run, not even an earlier attempt's deferred publish;
running it again reads it afresh and publishes that. A plain form (by its name, `<table> form` when it has
none, or its `formId`) that replaces a removed declared form with unfinished work (a lost create, an owed or
unconfirmed publish) is refused (`managed-form-unsettled`):
restore the declaration and let it settle, or run the build again later.
- **Declared dashboards** (`minimumPluginVersion: "2.21.0"`) — write `reconcile: "declared"` on a global
dashboard only when the maker asked its tiles to follow the spec on rebuilds, or said yes when you offered it at Level (b); give it and every tile a
`key`. Tell the maker: tiles the spec omits stay (nothing is deleted), and settings a tile does not state are
kept; a new tile is added after the last row; in an environment with more than one language a new tile gets
its caption, with the same text, in every language the environment has (if the build cannot read those
languages, it stops before it writes the dashboard: `dashboard-languages-unreadable`, so run it again; the
platform keeps some of those captions and drops others, an iframe tile's for one, and a dropped one other
than the base-language caption is no change); an
unfinished dashboard of the same name is not taken over
and not deleted; a dashboard this build cannot finish is not published, including by the app publish; a
dashboard the build created and could not prove is named by id in the stop: when the build knows it created it,
delete it and run the build again, which creates it anew; when the stop says the build cannot tell who created
it, have the maker inspect it and do not delete it; `dashboard-languages-unreadable` after a lost answer means
run the build again; and
Dataverse does not version a dashboard's saved layer, so an edit made in Maker at the instant of the write
cannot be detected. An existing dashboard is bound only with `--authorize adopt:dashboard:<key>`
(`overwrite:dashboard:<key>` or `overwrite:dashboard:<key>/<tile>` for changes made elsewhere,
`--allow-destructive` to clear its description with `""`): explain it and let the maker choose. `--verify`
holds a declared dashboard to its last build (a dashboard changed in Maker since fails until a build
settles it; one whose publish is unconfirmed is unverified, not passed). A download brings it back declared
only with `--include-managed --managed-manifest <store file>` (otherwise it is an undeclared entry), with the
dashboard and tile ids it was proved on; its first binding needs `adopt:dashboard:<key>`, because the
downloaded folder has no store.
- **Declared business rules** (`minimumPluginVersion: "2.21.0"`) — write `reconcile: "declared"` on a rule only
when the maker accepts that the build manages the WHOLE rule (the offer at the personas step, or their own ask); give every condition, action and apply-action a
`key`. Tell the maker: conditions and actions are never matched by their text; a new rule is created Draft and
activated only if nobody changed it in between; a rule that already exists is never edited by the build
(change it in Maker, then re-adopt it); a rule changed outside the spec stops the build until it is re-adopted
whole with `--authorize adopt:rule:<table>/<name>` and a declaration naming its whole body; and an environment
that cannot host business rules skips them. Explain the approval and let the maker choose. `--verify` holds a
declared rule to its last build (a rule changed in Maker since fails until it is re-adopted). A download
brings it back declared only with `--include-managed --managed-manifest <store file>` (otherwise it is left
out, as every business rule is); the first build that binds a rule that already exists needs
`adopt:rule:<table>/<name>`, because the downloaded folder has no store.
- **Declared business process flows** (`minimumPluginVersion: "2.21.0"`) — write
`reconcile: "declared"` on a flow only when the maker accepts that the build manages the WHOLE flow (the offer at the personas step, or their own ask); give every
stage and step a `key`. Tell the maker: a key keeps a stage's or step's id, so a rename keeps the records on it;
a new flow is created Draft and activated only if nobody changed it in between; a flow that already exists is
never edited by the build (to change its body or status, change it in Maker, then re-adopt it, keeping every
stage and step in place); removing or reordering a stage or step is always refused, and no change in Maker or
re-adoption unlocks that; a flow changed outside the spec stops the build until it is re-adopted whole with
`--authorize adopt:bpf:<table>/<name>` and a declaration naming its whole body. Explain the approval and let
the maker choose. `--verify` holds a declared flow to its last build (a flow changed in Maker since fails until
it is re-adopted). A download brings it back declared, keys included, only with `--include-managed
--managed-manifest <store file>` (otherwise it is left out); the first build that binds it from the downloaded folder
needs `adopt:bpf:<table>/<name>`, because that folder has no store. Changing only who may run a managed flow
(`securityRoles`) writes nothing to the flow; the build records it. A managed download restores a flow that has
`securityRoles` only when it can prove each persona the flow names from the role a build made for it, and then
writes that persona into `personas[]` (a NOTE names it). Tell the maker that persona now owns its role: a
privilege added to that role outside the spec is removed by the next build. When a persona cannot be proved,
the download leaves the flow out with a warning naming the personas and why (nothing is lost; the store and the
maker's own spec keep it) and writes the flow ready to paste in
the new file `managedFragments.file` names. Tell the maker to paste that flow and copy the
personas it names from the spec it was built from. Earlier fragments files are kept; never delete one
yourself. If the download stops with `managed-fragments-unwritable`, `.maker-workspace` is not a plain folder
or cannot be written: ask the maker to fix it, never move or delete it yourself. A
declared rule or flow removed from the spec whose row was deleted is no longer managed once the build reads it gone
by its id (a `managed-root-retired` warning, which never means it was activated); until then, a removed one whose
change is still waiting to be proved keeps the build incomplete. A retirement a build reports it could not
confirm also ends that build incomplete; tell the maker to run the build again, which reads the record afresh.
- **An unconfirmed publish** — a build that stops naming `--authorize publication-risk:table:<table>/<dispatch>`
(or `publication-risk:dashboard:<id>/<dispatch>`) lost the answer to a publish that may still complete.
Tell the maker to check what is live on that table; pass the key exactly as printed, from the latest stop,
only when they accept the risk, whichever step printed it. The key names that one lost publish: an older
key does not accept a later one, and a key that matches nothing is reported as not used. That run records
it before it writes anything and stops (`publication-risk-accepted`); the next build publishes after the
usual checks. A dashboard the spec no longer declares stays owed (`publication-withheld`) until its
declaration is restored. A run that cannot tell whether it recorded the key stops before any write
Voir sur GitHub