- 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 (page `.tsx` generated in generate-pages after plan approval), and an app module + sitemap — via the headless cds-maker-sdk. Runs an interactive, multi-turn authoring flow (env selection, jobs-to-be-done first, then design-only App Spec authoring across confirmed levels, guardrail lint, plan-mode approval, generate-pages, full build) and a narrated build, 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". 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.
# 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.
- **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.
- **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**.
- **AI-first features** (admin-gated) — form-fill assist, natural-language grid/view search, NL chart
/ AI data visualization, M365 Copilot (opt-in); per-table Copilot row summaries (Insight Cards)
with tailored prompts, auto-selected for good-candidate tables
Author the **smallest spec that fully satisfies the ask**, then let the user refine. The **Genpage-first
policy** below is the record-vs-dashboard rule; [`references/app-spec-schema.md`](../../references/app-spec-schema.md)
documents every field.
## Genpage-first policy (surface classification)
Every app surface is one of two kinds — **enumerate the app's surfaces and classify each one**, don't
decide page-by-page in passing:
- **Record surfaces** (create/read/update/list a table's rows) → a **model-driven form + view**.
- **Everything else** — overview/landing, dashboard, KPIs, analytics, guided or wizard flow,
composite or comparison screen → a **generative page** (`pages[]`), **not** a classic dashboard.
Rules:
- An app whose jobs include an overview, a queue, analytics or a guided flow but which proposes no
pages has missed a surface. Where the app genuinely is record-CRUD only, say so explicitly rather
than silently omitting pages.
- A **traditional `dashboards[]`** is emitted **only on explicit request** (e.g. "use a classic dashboard").
- A generative page is authored as a **design intent** (`source: { kind: "intent" }`,
`schemaVersion: 2`) during Phase 1 — its `.tsx` is written in **Phase 1.5 — Generate pages**, after
plan approval and after the data pre-build creates the tables so `pac model genpage generate-types`
can emit `RuntimeTypes.ts`. See [`references/authoring-flow.md`](../../references/authoring-flow.md) → *Pages*.
- The build's `pages` phase uploads each page via `pac model genpage upload` **without
`--add-to-sitemap`** — the SDK is the **single sitemap writer**, so a page's nav entry comes from a
`page` subarea in `appShell` (referenced by the page's **`key`**). See
[`references/app-spec-schema.md`](../../references/app-spec-schema.md) → `pages[]`.
- **Every page in `pages[]` must be sitemap-placed** — validation rejects any page absent from the
sitemap. A "detail" page that receives a caller-supplied id is a normal sitemap page; it reads its
input via `pageInput?.data?.<field>`. Navigation-only (headless) pages are not supported.
- **A page that declares `pageInput` MUST declare `directEntry`.** Because every page is
sitemap-placed, a detail page is also reachable straight from the app navigation with **no input**
— a state a user reaches by clicking. Say what happens then: `{ "behavior": "selector" }` shows a
picker and then the record, `{ "behavior": "emptyState" }` explains and renders nothing broken.
Prefer `selector` when the table is browsable; it is the more useful landing. Every key in
`pageInput.data` must also be produced by some page's `navigatesTo[].data`, or the generated page
reads a key nothing ever sets.
- **Three-authority page identity** (build + download + verify all follow this): (1) **IDENTITY** —
the durable `<app>_pagemanifest` (`key → pageId`); a downloaded spec's own `pages[].pageId`
outranks it for that rebuild. (2) **EXISTENCE** — env-wide `pac model genpage list` (crash-safe;
decides create-vs-reuse). (3) **MEMBERSHIP** — the app's sitemap `GenPageId` set (placement,
download enumeration, verify coverage). All matching is by id — never by display name.
- **Multi-page navigation uses `PAGEREF_<key>`** (the stable `pages[].key`) as the `pageId`
placeholder; the build resolves each to the real page GUID in a run-scoped staging copy (the
canonical `.tsx` is never mutated), then **verifies every nav edge** resolves to the deployed
page's `GenPageId`.
## Workflow
### Phase 0 — Working directory
1. Derive a short kebab-case slug from `$ARGUMENTS` (e.g. "Project Tracker" → `project-tracker`).
2. Create the directory (`mkdir -p <slug>` on bash/PowerShell; `mkdir <slug>` on cmd, which has no
`-p` and errors if it exists) and resolve its absolute path. It holds `app-spec.json`,
`model-app-plan.md`, and `workflow-log.md`.
### Phase 1 — Author the App Spec (interactive, main loop)
Follow **[references/authoring-flow.md](../../references/authoring-flow.md)** step by step, running
every prompt yourself via `AskUserQuestion`. In short:
#### Unattended runs (Copilot autopilot / automation)
Resolve the interaction mode once before the first authoring question:
```bash
node "${PLUGIN_ROOT}/scripts/resolve-interaction-mode.js" [--non-interactive]
```
`POWER_PLATFORM_SKILLS_NONINTERACTIVE=1`/`true` (or the equivalent
`--non-interactive` invocation) means there is no user waiting for prompts. In
that mode:
- do not call `AskUserQuestion`, `EnterPlanMode`, or `ExitPlanMode`;
- use explicit requirements and supplied existing specs as authoritative, and
use documented defaults only where the request leaves a non-destructive
choice open;
- record each skipped gate as
`Unattended default: <question> → <answer> (<reason>)` in `workflow-log.md`;
- treat the completed lint, preview, rendered design document, and build dry-run
as the approved plan, then continue with the normal approved path;
- halt rather than guessing an ambiguous environment, app identity, or
destructive structural edit.
Suppressing interaction never authorizes destructive work. An unattended
existing-app apply still requires `--allow-destructive` wherever the build
normally requires it; `POWER_PLATFORM_SKILLS_NONINTERACTIVE` only suppresses
prompts.
Carry the resolved mode through subprocesses: when it is unattended, append
`--non-interactive` to every `build-model-app.js` invocation (dry-run, data
pre-build, recovery rerun, and full apply). Do not append `--allow-destructive`
unless destructive authority was supplied independently.
1. **Prereqs** — `node --version`, `pac help` (> 2.10.0, the plugin's minimum).
2. **Environment (PAC)** — `pac auth list`. If exactly one / an active profile, **confirm it
(FYI), don't ask**. If several and none active, **ask** which to use. If none, ask the user
to `pac auth create`. Capture the org URL (`pac org who`).
3. **Detect existing** — `pac model list-tables --search …` (exact-match) and `pac model list`
to find tables/apps already present; build *around* them.
4. **Levelled authoring** — **first read the App Spec format** so you author to the exact
shape (do this once; don't go spelunking through scripts):
[`references/app-spec-schema.md`](../../references/app-spec-schema.md) and the worked sample
[`samples/app-spec.support-desk.json`](../../samples/app-spec.support-desk.json). That document
covers everything an app always has. **Additionally read
[`references/app-spec-schema-advanced.md`](../../references/app-spec-schema-advanced.md) once your
design calls for a conditional feature** — business rules, a business process flow, command-bar
buttons, web resources, global choices, classic dashboards, or `roleGrants[]`. Its pointer table
is in the main schema, and the toolbox above tells you when to reach for each; read the detail
only for the ones you are actually using, and never skip a capability just to avoid the read.
Phase 1 is
**design-only**: never emit page `.tsx` here. Each level is confirmed via `AskUserQuestion` before
the next begins, and `app-spec.json` is persisted after each — full prompts in the playbook.
- **Level (a0) — personas & jobs-to-be-done** (`personas[]`): **before proposing any tables**, ask
who will use the app and what each needs to get done. Jobs drive everything that follows — a
table exists because a job needs its data, a surface because a job needs to act on it. Deriving
the data model first reliably misses surfaces. Privileges come later; capture the jobs now.
- **Level (a) — data model**: entities/columns/relationships **derived from those jobs**; run the
**early data-model lint** (catches e.g. the relationship-vs-lookup collision before forms are
authored on top).
- **Descriptions are part of authoring, not a cleanup pass.** Every table, column, view, chart,
form, dashboard and business rule takes an optional `description`, and you should **write one as
you create the artifact** — never as a backfill. A name says what a thing is called; a
description says what it is *for*, and it is the only intent an app carries that an agent can
read back later when it inspects an app it did not build. Describe the purpose, not the shape:
`"Severity 1-5; drives the escalation rule and the SLA clock"`, not `"The priority column"`.
(`commands[]` and `Customer` columns accept one but the SDK cannot write it — you'll get a
warning; `personas[]` does not take one at all. See the schema reference for why.)
The app itself takes two: `app.description` is the short tile text, and `app.aiDescription` is
the **routing description** an orchestrator reads to choose between apps — who the app is for,
what it covers and excludes, and how to tell it from a sibling app over the same tables. Write
it whenever such a sibling exists or is planned.
- **Level (b) — artifacts + page-intents + design**: **enumerate every surface each job needs and
classify it** per the genpage-first policy above — record CRUD → form + view; anything else
(overview/landing, dashboard, KPIs, analytics, guided/wizard flow, composite or comparison
screen) → a page **intent** (`source: { kind: "intent" }`). State each classification out loud;
if the app needs no pages, say so and why rather than silently omitting them. Then forms + views
+ charts + sample data and the optional `design` contract, mapping each job to its surfaces via
`jobs[].surfaces[]`. **No page `.tsx` here.** `dashboards[]` only on explicit request.
- **Level (c) — access** (`personas[].jobs[].privileges[]`): personas and jobs already exist from
(a0) and the entities now exist, so **only add the privileges each job needs** — don't re-ask who
the users are. The builder unions them into one role per persona and grants the app to it so it
**opens for that persona, not just sysadmins**. **Render the roles + per-entity access as a table
in your chat reply** (the user can't approve an access model they can't see — see the CRITICAL
note above). If they want no roles, tell them jobs live in `personas[]` so dropping the roles
drops the recorded jobs too; offer read-only privileges to keep the record. (Column-level
security and access teams are not yet supported — see *Notes & limits*.)
- **Whole-app preview** (design gate for Level (b)): `node "${PLUGIN_ROOT}/scripts/preview-app.js" --spec @<working-dir>/app-spec.json`
renders data-model + sitemap + form wireframes + page-intents + design contract. **Reproduce the
ENTIRE rendered output verbatim in your chat reply, inside a fenced ` ``` ` code block — do NOT
leave it in the (collapsed, invisible) tool output, and do NOT just summarize "the preview looks
right" (see the CRITICAL note above).** The user must be able to SEE each form, the sitemap, and
View on GitHub