app-builder — intent → model-driven app skill

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.

by microsoft·MIT license·★ 954 Stars on the repo·GitHub ↗

Use now

Files of app-builder — intent → model-driven app

microsoft/main1 file shown
SKILL.md
Show the full text682 lines

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 → 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 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 → 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 → 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 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:

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 and the worked sample samples/app-spec.support-desk.json. That document covers everything an app always has. Additionally read 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 the page intents they are approving. For a single form only: node "${PLUGIN_ROOT}/scripts/preview-form.js" --spec @<working-dir>/app-spec.json.
    • Don't pre-create tables/columns — the build does it idempotently.
  5. Guardrail lint (hard gate) — run the full lint on the complete spec; errors block, warnings teach. If it blocks (or warns), paste the findings into your chat reply — tool output is collapsed and invisible to the user (see the CRITICAL note above), so the user can't fix what they can't see:
    node "${PLUGIN_ROOT}/scripts/lint-app-spec.js" --spec @<working-dir>/app-spec.json --json
    
    This runs migration and validateAppSpec — the gates the build itself runs on load — plus the lintAppSpec authoring guardrails, which the builder does not run. It exits non-zero on errors. It validates under the plan profile (which allows not-yet-generated intent pages), matching Step 6's dry run.
  6. Plan-mode approval (the single build approval) — present the plan including the build dry-run's phase-grouped plan (run build-model-app.js without --apply, using the plan profile that allows intent pages) inside EnterPlanMode, then ExitPlanMode to get the user's go-ahead. On approval, Phase 1.5 (generate-pages) runs first, then Phase 2 applies directly (no second dry-run/go-ahead). Render the design document — never hand-write it:
    node "${PLUGIN_ROOT}/scripts/write-app-spec-doc.js" --spec @<working-dir>/app-spec.json --env <envUrl>
    
    It writes <working-dir>/model-app-plan.md (jobs → surfaces traceability, data model, every surface, navigation, access model, sample data) and prints { ok, docPath, bytes, warnings }. Surface those warnings — they name design gaps such as a job with no covering surface or an app with no generative pages. Tell the user where the document is; it's theirs to keep, and it's regenerable after any spec edit.
Phase 1.5 — Generate pages (main loop, headless workers)

After plan-mode approval (before the full build):

  1. Data pre-build — schema-only build so generate-types can resolve real column names:

    node "${PLUGIN_ROOT}/scripts/build-model-app.js" \
      --env <envUrl> --spec @<working-dir>/app-spec.json --stage data --apply
    

    --stage data applies solution + data-model only — no --sample-data (rows are created once in the full build). Only --stage data is apply-safe; all other --stage selectors and legacy --from/--to/--only/--skip selectors are dry-run inspection only.

  2. Types — generate Dataverse type bindings for the entities the pages read:

    pac model genpage generate-types --data-sources "<entity1,entity2,…>" --output-file <working-dir>/RuntimeTypes.ts
    

    --data-sources is the union of every intent page's dataSources. Skip this step entirely when every intent page is mock-only. On Windows use forward slashes in the path.

  3. Page plan (adapter) — the page worker's input contract is a plan document, not an App Spec, so project the spec into one. This also echoes the per-page dispatch parameters:

    node "${PLUGIN_ROOT}/scripts/write-page-plan.js" \
      --spec @<working-dir>/app-spec.json --working-dir <working-dir> --env <envUrl> \
      --app "<app name>" --languages "<languages from the environment probe>"
    

    It writes <working-dir>/app-builder-page-plan.md and prints { ok, planPath, pages: [{ name, key, file, dataMode, intent }] }. Pass --languages through from the environment probe — omitting it silently defaults every plan to English-only and drops the localization pattern. The command fails (before writing) if the plan would name a sample that doesn't exist, or if a page a worker will write is not a safe target in the working directory — a link at its path, a folder that links outside the directory, or a page already there under another spelling — or if a link, a hard link or a folder is already at the plan's own path. Stop and fix what it names; do not dispatch any worker.

  4. Generate — for each page from step 3 with intent: true, dispatch the headless genpage-page-builder worker via Task. Use its documented input contract verbatim — a missing field is why a page silently never becomes .tsx:

    You are the genpage-page-builder agent. Generate the [name from step 3] page.

    • Target file: [file from step 3 — already includes .tsx; do NOT append another]
    • Plan document: [absolute path to the app-builder-page-plan.md written in step 3]
    • Data mode: [dataMode from step 3 — dataverse or mock]
    • Connectors: none
    • Telemetry: disabled
    • RuntimeTypes: [absolute path to RuntimeTypes.ts] ← omit this line when Data mode is mock
    • Working directory: [absolute working-dir path]
    • Plugin root: ${PLUGIN_ROOT}

    Follow the instructions in your agent file. Write [file] and return your result when done.

    The plan's ## Environment carries Mode: app-builder and every page row carries a Key, so the worker emits "PAGEREF_<key>" for cross-page navigation (never a file-derived token — a downloaded page's codeFile is a path, not its identity). Custom nav ids go in data: — never recordId. Connectors: none and Telemetry: disabled are constants here: the App Spec has no connector-binding concept (so the projected plan always says No connector bindings.), and /app-builder never runs the Phase 4.7 custom-telemetry probe. Both are stated explicitly rather than omitted — the page-builder treats a missing line as the same fail-closed value, but an explicit line is what makes the dispatch contract checkable.

  5. Validate + commit the transition (transactional) — never flip source by hand, and never flip pages one at a time as workers return. Run:

    node "${PLUGIN_ROOT}/scripts/promote-intent-pages.js" \
      --spec @<working-dir>/app-spec.json --working-dir <working-dir>
    

    It checks every generated page (file written, structurally a module, PAGEREF_ tokens canonical and in exact parity with the spec's navigatesTo edges) and only then flips all of them intent → { kind: "tsx", codeFile } in a single atomic write. On any failure it exits 3 and leaves app-spec.json untouched, printing which page failed and why — regenerate just those pages and re-run. All-or-nothing on purpose: a half-flipped spec would claim a .tsx that was never written, and Phase 2's deploy profile fails fast on any remaining source.kind === "intent".

  6. Proceed to Phase 2 (full idempotent build).

⚠️ The interactive author never runs inside a Task subagent. Only pure, headless code-gen workers are dispatched here — all user-facing prompts originate in the main loop.

Phase 2 — Build (narrated, main loop)

Always use scripts/build-model-app.js. Never hand-write a builder. It's idempotent (skips existing solution/tables/columns/relationships — so new, existing, and mixed envs all just work), so you don't pre-create anything or special-case existing tables.

The build plan was already presented and approved in plan mode (Phase 1 Step 6 shows the engine's real dry-run plan), so on approval apply directly — one build approval, no second go-ahead. Add --verify so the build self-checks after applying (see Phase 3):

node "${PLUGIN_ROOT}/scripts/build-model-app.js" \
  --env <envUrl> --spec @<working-dir>/app-spec.json --apply --verify [--sample-data] [--publish]

Each step streams its status live ([n/total] ✓ created / ⊘ skipped / ✗ failed — <error>) and a closing ✓ build complete — X created, Y skipped, Z failed summary.

Keep the build's progress visible. A full build runs for several minutes. Let its output stream — do NOT pipe it through Select-Object -First/-Last N or Select-String head-limits, which buffer and hide progress until the run ends (and can truncate a still-running pipe). To capture the log use Tee-Object -FilePath <log> (no head-limit). The build also prints a ▸ live progress: line pointing at <workspace>/.maker-workspace/build-status.json — a snapshot (state, steps, lastPhase, lastLabel) overwritten every step. Read it (or tail build-log.jsonl) any time to report where a long build is, even if stdout is buffered.

(Reaching Phase 2 without a fresh plan-mode approval — resuming a failed build, or a quick edit re-run — do a dry-run first (drop --apply), paste the phase-grouped plan verbatim into your chat reply (tool output is collapsed — see the CRITICAL note above), and get a go-ahead before applying.)

Stage selector (--stage <data|ui|app|publish>) maps to its phase range. On --apply, ONLY --stage data is accepted (solution + data-model, no rows in run 1; run 2 is a full build). All other stages and the legacy --from/--to/--only/--skip selectors are dry-run inspection only — their phase ranges are not dependency-closed and are rejected on --apply.

Language (--language-code / --languageCode <lcid>) — you normally do not pass this. Data-model labels are stamped with the organization's own base language, read once per build. Pass it only to author labels in a different provisioned language, or if the build warns that it could not determine the base language and fell back to 1033.

If you pass an LCID the organization has not provisioned, the build now stops in the data-model phase, before any label is written, and lists the ones it does have. That check exists because Dataverse handles this inconsistently: table and choice labels are accepted (HTTP 204) and silently stored under the organization's base language, while DateTime and Memo columns are rejected with "The language code N is not a valid language for this organization". Without the check a build therefore creates the table with the wrong labels and then dies partway through the columns, phases away from the flag that caused it — which reads like an environment fault. List the provisioned set with node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET RetrieveProvisionedLanguages. The check is best-effort: if that read fails the build proceeds unchanged.

The App Spec field languageCode pins the same value across runs. It now covers the whole build: data-model labels, and form, dashboard and sitemap labels — the SDK serializers that used to hardcode 1033 take the authoring language as an option.

Narrate progress as it runs. Transient env errors (429 customization-lock, 503 SQL-timeout, a SQL deadlock, concurrent-op guards) are auto-retried with backoff on --apply (the build is idempotent, so a retry reuses what's already created). If the build still halts (BuildHalt) on an unrecoverable error, surface it and ask the user how to proceed via AskUserQuestion (adjust the spec / cancel), then re-run. Everything is scoped to a dedicated unmanaged solution; --publish gates the final bulk publish — edit/finalize paths still publish their one targeted artifact so the change takes effect (see Notes & limits).

Recovery from a failed or halted build: run the full build again. The build is idempotent — every phase re-uses what's already created and only fills the gaps. SDK metadata is persisted under <working-dir>/.maker-workspace/ (override with --workspace). There is no apply-safe --from <phase> shortcut; a full rerun is the correct and safe recovery path. One exception: an already-exists halt recurs on a rerun until that workspace directory is deleted — follow its message.

Phase 3 — Verify & iterate

--apply --verify already reconciled the spec against what deployed (Phase 2) — the build appends a verify PASS / verify FAIL — N missing line and exits non-zero on a silent partial (an artifact created but not wired, or a phase that quietly produced nothing). To re-check later — e.g. after a Maker change, without rebuilding — run the standalone read-only verifier (entities/columns/views/charts/ forms and sitemap subareas + icons; exits non-zero and lists anything missing):

node "${PLUGIN_ROOT}/scripts/verify-model-app.js" --env <envUrl> --spec @<working-dir>/app-spec.json

Then open the app in the browser. Refine app-spec.json and re-run Phase 2 to iterate.

Teardown (cleanup). To remove everything an App Spec built — e.g. a live-verification probe or a failed build — run the classifier-safe teardown. It deletes only the artifacts the spec declares, in dependency order (app module → dashboards → command bars → forms → security roles → charts → views → reset enriched default views to drop parent lookups → relationships → AI row summaries → tables [children-first] → web resources (generated app icon + page manifest + declared) → global choices → solution). Forms/charts/views/relationships are deleted explicitly before tables (a table delete does not reliably cascade cross-references; it does remove the table's own columns). Security roles come after forms because a form assigned to a role names it inside its own formxml, which the platform counts as a dependency, and before relationships/tables because a role holding a table's privileges can block that table's delete. Command teardown removes the whole command bar for any entity the spec authored commands on. Teardown only deletes tables this build created — a system/standard table (account, contact, …) is auto-detected and skipped, and a reused custom table is skipped when its entity is flagged "existing": true, so pre-existing data survives. The same flag protects relationships and global choices — a download sets it on every one it recovers, so tearing down a downloaded spec never removes a lookup column from a retained table or deletes a shared option set. Dashboards are found by name, so teardown deletes only those the app's solution holds, never another app's namesake — and none when the spec has no real solution to ask (the Default a download may leave); it keeps the solution itself while any step failed, so a re-run can still tell, and keeps the page manifest while the generative-pages step failed, so a re-run still finds the pages the app authored. Dry-run by default; add --apply --allow-destructive to actually delete (--clear-workspace also prunes .maker-workspace/). --allow-destructive is required for teardown --apply — without it teardown refuses and touches nothing.

node "${PLUGIN_ROOT}/scripts/teardown-model-app.js" \
  --env <envUrl> --spec @<working-dir>/app-spec.json [--apply] [--allow-destructive] [--clear-workspace]

Safety flags (build + teardown). The apply path is fail-closed against destructive operations:

  • --allow-destructive — authorize destructive operations. For build --apply: authorizes overwriting an existing app in unattended mode, and allows explicit-layout form-field removals or sitemap-target drops; also authorizes DETACHING a pages-removed page's nav subarea (the page record is left deployed — it is not deleted). For form and sitemap removals, the authority covers the removals the last refusal or approved run recorded; if live state would lose anything more, the apply halts, lists only the newly destructive removals, and records that new list for review. A field or sitemap target that appears during a run is kept or halted before push, never removed unseen. The record is consumed only after a full successful build that ran the removal phases and kept nothing; it is kept on failures, partial runs, changed-only runs, unreadable records, and failed discovery. For teardown --apply: required — all deletes are destructive by construction, so teardown without this flag halts before touching anything.
  • Pages-phase safety HALTs. The build halts on identity or safety violations rather than proceeding with potentially wrong state. Surface the HALT reason and follow the recovery hint:
    • pages-identity-conflict — spec pageId and manifest disagree on a key, or a duplicate id spans two keys. Resolve the conflict manually (re-download or delete the manifest).
    • pages-manifest-corrupt — the manifest cannot be parsed (two keys map to the same id). Delete the manifest web resource and rebuild from scratch.
    • pages-removed — a live page was dropped from the spec. Re-add it, or pass --allow-destructive to detach it from the nav (the page record stays deployed; rebuild finalizes the sitemap).
    • pages-shared-across-apps — the page appears in another app's sitemap. Detach it in Maker. --allow-destructive does not bypass this halt.
    • pages-shared-check-failed / pages-existence-failed / pages-sitemap-read-failed — a prerequisite read failed; the build can't proceed safely. Retry on a transient error; check permissions on a persistent one.
  • --non-interactive — suppress interactive prompts (for automation / CI). A non-interactive build that encounters an existing app fails instead of warning-and-proceeding, unless --allow-destructive is also set. Does not grant destructive authority on its own — only --allow-destructive does.
  • POWER_PLATFORM_SKILLS_NONINTERACTIVE=1 (or true) — env-var equivalent of --non-interactive. Same semantics: suppresses prompts only, never authorizes destructive ops. Set this in CI job environments to avoid interactive-prompt hangs.
  • In autopilot / eval mode (--non-interactive + --allow-destructive), preview-app.js is written to disk as the design artifact before plan execution; interactive consent gates are bypassed and the build is fail-closed against any destructive op not explicitly authorized.
AI-first features

The ai block in the App Spec controls four app-level features and per-table Copilot row summaries. All features are admin-gated: they are enabled only where the environment administrator has turned them on in Power Platform Admin Center (Environments → Settings → Product → Features). The ai-features build phase preflights each setting via the SDK (RetrieveSetting) and, for anything off, skips it with a warning — it never fails the build and cannot flip admin or tenant switches itself.

Preflight (standalone):

node "${PLUGIN_ROOT}/scripts/ai-preflight.js" --env <envUrl> [--app <uniqueName>]

Prints each feature's on/off status and the exact admin action required for anything that is off. Never fails.

App-level features (ai.appFeatures) — formFill (Copilot-assisted form fill), nlSearch (natural-language grid/view search), nlChart (NL chart / AI data visualization), m365 (M365 Copilot). All default to on except m365, which is left at its platform default; false writes an explicit Off for this app (each setting's values: references/app-spec-schema.md → ai).

Per-table row summaries (ai.summaries):

  • default: "auto" — the skill auto-selects good-candidate tables (skips lookup-only / config / junction tables and the Dynamics 365-owned incident, lead, opportunity).
  • default: "off" — summaries disabled unless a table opts in explicitly.
  • Per-table overrides in summaries.tables: set enabled, a tailored instruction, and columns[] (the fields the summary reads). A { "enabled": false } entry opts a specific table out.

Prompt authoring guidelines (for instruction): write for meaningful insights — not field/value repetition; never include record GUIDs; pull in recent activity where relevant; use audience-appropriate tone; aim for an explicit output shape (a short paragraph).

Teardown removes the AI records created by the ai-features phase (summary config rows and published AI models) in addition to the standard artifacts.


Editing a deployed app (download → edit → rebuild)

The same App Spec drives edit — there is no separate edit path. When the user wants to change an existing app (add a field/view/page, edit a page's code, retitle/reorder nav, swap an icon), pull the deployed app fresh into a spec first, then edit that spec and re-run Phase 2:

node "${PLUGIN_ROOT}/scripts/download-model-app.js" --env <envUrl> --app <appId|uniqueName|displayName> --out <working-dir>

--app resolves in identity order: app id (GUID) → uniquename → display name. Prefer the unique name — it is immutable, while a display name can be renamed and is not unique. A display name that matches more than one app is refused, listing the candidate unique names rather than guessing.

This reconstructs the app into <working-dir>/app-spec.json. Round-trip scope — be precise, it is not everything:

  • Round-trips: the sitemap → appShell (all subareas + icons), every generative page (via pac model genpage download; the pages are the sitemap's GenPage subareas, so Maker-added pages are included, and each keeps its own name and model) into pages[] + their .tsx, the referenced entities (minimal — the build reuses existing tables), classic dashboards (id-passthrough tiles carrying the deployed view/chart ids), the icon web resources, and the solution.
  • Does NOT round-trip: forms[], views[], charts[], commands[] — they come back empty. All four survive on the live app (a rebuild preserves them by discovery), so a plain edit is safe; they just aren't editable through the downloaded spec. Change them in Maker or a fresh spec.

Then:

  1. Always pull fresh at the start of an edit session — someone may have changed the app in Maker. The build reads an etag when it hydrates, so a write against an artifact changed since the pull throws a version conflict → re-pull and retry, never clobber.
  2. Edit app-spec.json (and any page .tsx) for the requested change.
  3. If the edit ADDS a page (or resets a page's source back to intent to regenerate it), re-run Phase 1.5 before Phase 2 — a downloaded spec contains only kind: "tsx" pages, so an added intent page has no code and Phase 2's deploy profile rejects it. Phase 1.5 is safe to re-run: write-page-plan.js projects all pages (so the worker sees the real nav graph), a worker is dispatched only where the echo says intent: true, and promote-intent-pages.js leaves built pages untouched. Editing an existing page's .tsx by hand needs no regeneration.
  4. Re-run the build (Phase 2). It's idempotent: it reuses the existing app/tables/views, updates each page in place (matched by name → --page-id, so no duplicates), and preserves the existing GenPage subareas (the download enumerated them into pages[]/appShell, so the full-replace sitemap write never drops them).
  5. Verify (Phase 3) to confirm only the intended change landed.

Prefer generative pages over classic dashboards per the genpage-first policy. Classic dashboards do round-trip (id-passthrough tiles), but the views and charts their tiles point at do not, so they can only be edited in Maker.

What the builder does (in order)

solution (idempotent) → data model — discover existing tables/columns/relationships via the SDK (findTables / findColumns / fetchEntityMetadata) and create only what's missing (createTable / createColumn / createRelationship) → sample data (opt-in; relational/topological, $parent→@odata.bind using the entity-set name) → web resources (opt-in; createWebResource for form JS/HTML/CSS) → views → charts → forms (primary + columns laid out, explicit tabs honored; sub-grids, quick-views, and form JS (events[]) applied as canonical control cells / the /bag/c events region via the SDK's generic addElement surface) → business rules (businessRules[] — authored as the workflow object model through the bound CreateProcessWithWfomJson member and activated; skipped with a warning on an environment that does not declare that member) → business process flows (businessProcessFlows[]; a category-4 process compiled from the authored stages and activated — this goes through the SDK's generic artifact surface, a plain workflows row, so it is NOT subject to the bound-member gate above) → command bar (commands[]) → classic dashboards (opt-in) → app module + sitemap → generative pages (each page's .tsx was generated in Phase 1.5; the build uploads each pages[] page via pac model genpage upload, no --add-to-sitemap; then the SDK rewrites the sitemap once to add the GenPage subareas) → AI features (opt-in) → security (one role per personas[] entry, sized from its jobs' declared access; injects an app-module read privilege and associates the app to each role so it opens for that persona) → publish (opt-in). When the app has generative-page subareas the app module is created first WITHOUT them (they can't resolve until the pages upload), then the pages phase rewrites the sitemap. All Dataverse access goes through the SDK, so the downloaded metadata lands in .maker-workspace/. Independent ops (columns, views/charts/forms) run with bounded parallelism; publish is one round-trip per entity + the app. Views/charts build before forms so a sub-grid can reference the child view id. Each step emits [n/total].

Notes & limits

  • Headless, no browser. The SDK (cds-maker-sdk, vendored at scripts/vendor/) generates designer-grade FormXML/FetchXML/sitemap by reusing the designer's own serializers, and writes via the Web API using an az-token HttpClient. No relay, no designer tab.
  • Dedicated unmanaged solution per app (review / teardown). --publish gates the final bulk publish of the app's entity + app customizations (one PublishXml for every entity and the app, which also carries the default-view enrichment's publish, deferred to it). It does not suppress the small targeted publishes that edit/finalize paths must run so the change takes effect — reconciling an existing form or view, wiring form events, placing quick-views, re-syncing an existing app's sitemap, and finalizing the sitemap after generative pages each publish that one artifact (an unpublished edit to a live artifact is invisible). A fresh build without --publish still leaves new tables/columns/relationships staged-but-unpublished in the solution.
  • Idempotent — but not full desired-state convergence. Existing solution/tables/columns/relationships/views/charts/forms/commands/dashboards are detected and reused, so re-runs and existing-table envs work without collisions. The caveat for EDITS: a rebuild never removes an artifact you dropped from the spec, and re-applies only part of an edit to one that already exists. A form converges its fields (added; for an explicit layout, dropped ones pruned — see prune: false below), column counts and spans. A view only adds columns: a column removed from the spec, or a new column order, does not apply, and neither do its filters or sort. A changed column type, and an edited command or dashboard, are not re-applied at all; an existing chart takes only a changed description. To apply one of those edits, change it in Maker, or teardown --apply then rebuild fresh. --verify catches what is missing — a spec view column, filter condition or sort key (authored sort keys must also keep their order), a relationship, command or dashboard — as a loud verify FAIL. It tolerates extra deployed content, so a view column, filter or sort key you removed from the spec still passes, just as one a maker added by hand does.
  • Not in scope (later): conditional command visibility (Power-Fx-only), titled command groups (from-scratch — needs an SDK-synthesized parent row), lookup/associated views, multi-area sitemaps, column-level (field) security, and access teams / hierarchy security (both tracked SDK follow-ups). The security surface today is role-per-persona plus per-form role assignment, and both of those ship. Also out of scope: the BPF knobs the spec gate rejects (cross-entity stages, branching, stage actions).
  • Environment-gated (may not work where you are running):
    • Business rules (businessRules[]). The SDK writes a rule through the bound CreateProcessWithWfomJson member — the same one the modern business-rule designer uses — and has no fallback: the client-side workflow-XAML compiler it used to fall back on was removed upstream because it covered 4 of the 7 action types and a single clause, so it silently narrowed a rule into something that did not say what the author wrote. Environments that do not declare the member therefore cannot host business rules at all — and that is the common case, not an edge case. The build skips the rules, warns once naming the member, and builds everything else normally, so an app is never left half-created. If rules are essential, verify the environment first. Business process flows are NOT affected by this gate — a BPF is written as an ordinary workflows row through the SDK's generic artifact surface, with no bound member involved.
  • Supported: the full data model — all column types, AutoNumber primary, global choices, status reasons, alternate keys, N:N + junction-with-payload; required reconciled on existing columns (an explicit required converges on rebuild; an omitted one never demotes); Boolean defaultValue, whole-number integerFormat (incl. Duration), and per-column write permissions (isValidForCreate / isValidForUpdate / isValidForRead — this is how you make a column read-only), all applied on create and on rebuild; adaptive main forms with 1:N / N:N sub-grids; per-field form control (readOnly / hidden / after positioning, plus prune: false to edit a subset of a form non-destructively); quick-create / quick-view forms (formType) + quick-view placement (forms[].quickViews[]); per-form security roles (forms[].securityRoles — name the personas[] this form is offered to, or everyone: true; applied after the roles exist. A form with no assignment is visible to every role, so this restricts a form rather than granting it); which form a table opens with (forms[].isDefault / entities[].mainFormOrder set the table's Main Form Set order, on existing tables too — a user's remembered form still opens first for that user; see app-spec-schema.md); Choice-column charts; business rules (businessRules[] — authored as the modern workflow object model and activated; see the environment gate above); security roles (personas[] — one role per persona sized from its jobs-to-be-done, with app access so the app opens for non-admins); dashboards (dashboards[] — chart/list/iframe/webresource tiles) + dashboard sitemap placement; generative pages (pages[] — the genpage-first default, uploaded via pac model genpage upload and surfaced as GenPage sitemap subareas; full create + edit round-trip via download-model-app.js); modern command-bar buttons (commands[]) incl. flyout / split-button menus; rich view filters (eq-userid/this-week/in/not-in); custom grid rendering (entities[].columns[].visualization — radial dial / line chart / heat map / star rating, preview); web resources + form JS event handlers; sample data with multi-parent $parents + statusReason. See docs/app-builder-capabilities.md and references/app-spec-schema.md (plus app-spec-schema-advanced.md for the conditional fields it points at) — author from those docs; you should not need to read the SDK, lint, or engine to write a spec.
1---
2name: app-builder
3version: 1.0.0
4description: 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.
5author: Microsoft Corporation
6argument-hint: "<app description>"
7user-invocable: true
8allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Task, AskUserQuestion, EnterPlanMode, ExitPlanMode, TaskCreate, TaskUpdate, TaskList, read, edit, execute, search, agent, todo
9---
10 
11> **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.
12 
13# app-builder — intent → model-driven app
14 
15Turn a natural-language intent into a deployed model-driven app. You author a reviewable **App Spec**
16(JSON) with the user across confirmed turns, then a deterministic engine (`cds-maker-sdk`, vendored)
17builds it — tables/columns/relationships, sample data, views, Choice-column charts, adaptive forms
18with sub-grids, **generative pages** for overview/dashboard surfaces, and the app module + sitemap.
19The **same spec drives create and edit**: download a deployed app back into a spec, change it, and
20re-run the build (it's idempotent).
21 
22## CRITICAL — run the interactive flow in THIS conversation (the main loop)
23 
24> **You MUST run the authoring questions and the build narration yourself, in the main
25> conversation. Do NOT dispatch a subagent (`Task`) for the interactive steps.**
26>
27> A subagent is headless — `AskUserQuestion` and plan mode do not reach the user from inside one
28> (its only output is its final message). The whole point of this skill is the multi-turn,
29> propose-then-confirm experience, so every `AskUserQuestion`, `EnterPlanMode`, and live build
30> status line must originate here, in the main loop.
31 
32## CRITICAL — the user sees your chat message, NOT tool output
33 
34> **Shell/tool output — the result of running `preview-app.js`, a dry-run plan, or a lint —
35> is COLLAPSED BY DEFAULT in the UI. The user does NOT see it unless they manually expand the
36> tool panel.** Running the command is therefore NOT the same as showing the user. Every artifact
37> the user must **read, review, or approve** — the whole-app **preview wireframes**, the
38> **dry-run build plan**, and blocking **lint findings** — MUST be reproduced **verbatim in your
39> chat reply**, inside a fenced ` ``` ` code block. Never say "the preview looks right" and leave
40> the content buried in a collapsed panel: **paste it into your message.** This is the #1 cause of
41> "the wireframes aren't visible" — the preview ran, but its output stayed hidden.
42 
43## Capabilities — the full toolbox (pick best-fit per requirement)
44 
45You are a **complete** model-driven app builder, not a single-surface tool. Everything below ships in
46one App Spec and one build — choose what best serves the user's requirement to make a **useful,
47prod-ready** app; don't under-build (a bare table list) or over-build (surfaces nobody asked for):
48 
49- **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
50- **Record UI** — forms (sub-grids, quick-create / quick-view), views (with enriched default columns), charts
51- **Custom grid rendering** (preview) — `entities[].columns[].visualization`: render a column as a
52 `RadialDial`, `LineChart`, `HeatMap` or `StarRating` in **every** grid and view that shows it,
53 instead of plain text. Reach for it when a column is a *magnitude a user scans* (a score, a
54 utilization %, a rating, a priority) rather than a value they read exactly — it makes a list
55 scannable at a glance for one line of spec. It is per-*column*, so set it once on the column, not
56 on each view. **Preview:** on an environment where it is not provisioned the build skips it and
57 everything else still deploys, so it is always safe to include.
58- **Actions** — modern command-bar buttons (incl. flyout / split menus), web resources (form JS / HTML / CSS).
59 Two rules when writing that JS, both learned from buttons that deployed perfectly and then did
60 nothing: a command handler is handed the record (`function doThing(primaryControl)`) — the build
61 supplies the parameter, so write that signature; and **never hardcode Choice values** like
62 `100000003`, because they are assigned per publisher. Resolve by label via `getOptions()`
63 (see `references/app-spec-schema-advanced.md` → webResources). Note also that command and web-resource
64 **edits do not redeploy on rebuild** — the phases reuse what exists, so changing a button or a
65 script means deleting it first.
66- **Form logic without code** — `businessRules[]`: show/hide, lock/unlock, set-required and
67 set-value, driven by a condition on the record. **Reach for a business rule before form JS** when
68 the requirement is field-level and declarative — it is visible in the maker, survives solution
69 export, and needs no web resource. Use form JS when the logic needs a real API call, cross-record
70 work, or anything beyond the four supported actions.
71- **Guided processes** — `businessProcessFlows[]`: the staged bar across the top of a record
72 (ordered stages, each with steps bound to that table's columns — **every step must bind a `field`**;
73 the platform rejects one without, so use a Boolean flag for a manual check-off). Reach for one when
74 the user describes work moving through **phases** — "triage →
75 investigate → resolve", "lead → qualify → close". Author it `Active` (the default) or the stage bar
76 does not appear at all. Who may run a flow is `securityRoles: { "personas": [...] }` (Active flows
77 only). v1 is single-entity and linear: cross-entity stages, branching and stage actions are
78 **rejected** by the spec gate, so offer Maker for those rather than writing them into the spec.
79- **Surfaces** — **generative pages** (modern dashboards / overviews / analytics / landing — the default),
80 classic dashboards (opt-in), external URLs
81- **App shell** — the app module + sitemap, with per-subarea icons. Turn on the **modern shell** with
82 `app.newLook: true` unless the user asks for the classic one; it is opt-in and best-effort, so a
83 tenant without the setting still gets a working app. `app.headerNavigationRefresh` controls the
84 **Wave 2 header/navigation refresh** — a *separate, independent* setting whose platform default is
85 **ON**, so set it to `false` only when the user explicitly wants the classic header.
86- **Security & access** — one **security role per persona**, sized from that persona's jobs-to-be-done
87 (the entity access each job needs, unioned into the role), so the app **opens for non-admins**.
88- **AI-first features** (admin-gated) — form-fill assist, natural-language grid/view search, NL chart
89 / AI data visualization, M365 Copilot (opt-in); per-table Copilot row summaries (Insight Cards)
90 with tailored prompts, auto-selected for good-candidate tables
91 
92Author the **smallest spec that fully satisfies the ask**, then let the user refine. The **Genpage-first
93policy** below is the record-vs-dashboard rule; [`references/app-spec-schema.md`](../../references/app-spec-schema.md)
94documents every field.
95 
96## Genpage-first policy (surface classification)
97 
98Every app surface is one of two kinds — **enumerate the app's surfaces and classify each one**, don't
99decide page-by-page in passing:
100 
101- **Record surfaces** (create/read/update/list a table's rows) → a **model-driven form + view**.
102- **Everything else** — overview/landing, dashboard, KPIs, analytics, guided or wizard flow,
103 composite or comparison screen → a **generative page** (`pages[]`), **not** a classic dashboard.
104 
105Rules:
106 
107- An app whose jobs include an overview, a queue, analytics or a guided flow but which proposes no
108 pages has missed a surface. Where the app genuinely is record-CRUD only, say so explicitly rather
109 than silently omitting pages.
110- A **traditional `dashboards[]`** is emitted **only on explicit request** (e.g. "use a classic dashboard").
111- A generative page is authored as a **design intent** (`source: { kind: "intent" }`,
112 `schemaVersion: 2`) during Phase 1 — its `.tsx` is written in **Phase 1.5 — Generate pages**, after
113 plan approval and after the data pre-build creates the tables so `pac model genpage generate-types`
114 can emit `RuntimeTypes.ts`. See [`references/authoring-flow.md`](../../references/authoring-flow.md) → *Pages*.
115- The build's `pages` phase uploads each page via `pac model genpage upload` **without
116 `--add-to-sitemap`** — the SDK is the **single sitemap writer**, so a page's nav entry comes from a
117 `page` subarea in `appShell` (referenced by the page's **`key`**). See
118 [`references/app-spec-schema.md`](../../references/app-spec-schema.md) → `pages[]`.
119- **Every page in `pages[]` must be sitemap-placed** — validation rejects any page absent from the
120 sitemap. A "detail" page that receives a caller-supplied id is a normal sitemap page; it reads its
121 input via `pageInput?.data?.<field>`. Navigation-only (headless) pages are not supported.
122- **A page that declares `pageInput` MUST declare `directEntry`.** Because every page is
123 sitemap-placed, a detail page is also reachable straight from the app navigation with **no input**
124 — a state a user reaches by clicking. Say what happens then: `{ "behavior": "selector" }` shows a
125 picker and then the record, `{ "behavior": "emptyState" }` explains and renders nothing broken.
126 Prefer `selector` when the table is browsable; it is the more useful landing. Every key in
127 `pageInput.data` must also be produced by some page's `navigatesTo[].data`, or the generated page
128 reads a key nothing ever sets.
129- **Three-authority page identity** (build + download + verify all follow this): (1) **IDENTITY** —
130 the durable `<app>_pagemanifest` (`key → pageId`); a downloaded spec's own `pages[].pageId`
131 outranks it for that rebuild. (2) **EXISTENCE** — env-wide `pac model genpage list` (crash-safe;
132 decides create-vs-reuse). (3) **MEMBERSHIP** — the app's sitemap `GenPageId` set (placement,
133 download enumeration, verify coverage). All matching is by id — never by display name.
134- **Multi-page navigation uses `PAGEREF_<key>`** (the stable `pages[].key`) as the `pageId`
135 placeholder; the build resolves each to the real page GUID in a run-scoped staging copy (the
136 canonical `.tsx` is never mutated), then **verifies every nav edge** resolves to the deployed
137 page's `GenPageId`.
138 
139## Workflow
140 
141### Phase 0 — Working directory
1421. Derive a short kebab-case slug from `$ARGUMENTS` (e.g. "Project Tracker" → `project-tracker`).
1432. Create the directory (`mkdir -p <slug>` on bash/PowerShell; `mkdir <slug>` on cmd, which has no
144 `-p` and errors if it exists) and resolve its absolute path. It holds `app-spec.json`,
145 `model-app-plan.md`, and `workflow-log.md`.
146 
147### Phase 1 — Author the App Spec (interactive, main loop)
148 
149Follow **[references/authoring-flow.md](../../references/authoring-flow.md)** step by step, running
150every prompt yourself via `AskUserQuestion`. In short:
151 
152#### Unattended runs (Copilot autopilot / automation)
153 
154Resolve the interaction mode once before the first authoring question:
155 
156```bash
157node "${PLUGIN_ROOT}/scripts/resolve-interaction-mode.js" [--non-interactive]
158```
159 
160`POWER_PLATFORM_SKILLS_NONINTERACTIVE=1`/`true` (or the equivalent
161`--non-interactive` invocation) means there is no user waiting for prompts. In
162that mode:
163 
164- do not call `AskUserQuestion`, `EnterPlanMode`, or `ExitPlanMode`;
165- use explicit requirements and supplied existing specs as authoritative, and
166 use documented defaults only where the request leaves a non-destructive
167 choice open;
168- record each skipped gate as
169 `Unattended default: <question> → <answer> (<reason>)` in `workflow-log.md`;
170- treat the completed lint, preview, rendered design document, and build dry-run
171 as the approved plan, then continue with the normal approved path;
172- halt rather than guessing an ambiguous environment, app identity, or
173 destructive structural edit.
174 
175Suppressing interaction never authorizes destructive work. An unattended
176existing-app apply still requires `--allow-destructive` wherever the build
177normally requires it; `POWER_PLATFORM_SKILLS_NONINTERACTIVE` only suppresses
178prompts.
179 
180Carry the resolved mode through subprocesses: when it is unattended, append
181`--non-interactive` to every `build-model-app.js` invocation (dry-run, data
182pre-build, recovery rerun, and full apply). Do not append `--allow-destructive`
183unless destructive authority was supplied independently.
184 
1851. **Prereqs** — `node --version`, `pac help` (> 2.10.0, the plugin's minimum).
1862. **Environment (PAC)** — `pac auth list`. If exactly one / an active profile, **confirm it
187 (FYI), don't ask**. If several and none active, **ask** which to use. If none, ask the user
188 to `pac auth create`. Capture the org URL (`pac org who`).
1893. **Detect existing** — `pac model list-tables --search …` (exact-match) and `pac model list`
190 to find tables/apps already present; build *around* them.
1914. **Levelled authoring** — **first read the App Spec format** so you author to the exact
192 shape (do this once; don't go spelunking through scripts):
193 [`references/app-spec-schema.md`](../../references/app-spec-schema.md) and the worked sample
194 [`samples/app-spec.support-desk.json`](../../samples/app-spec.support-desk.json). That document
195 covers everything an app always has. **Additionally read
196 [`references/app-spec-schema-advanced.md`](../../references/app-spec-schema-advanced.md) once your
197 design calls for a conditional feature** — business rules, a business process flow, command-bar
198 buttons, web resources, global choices, classic dashboards, or `roleGrants[]`. Its pointer table
199 is in the main schema, and the toolbox above tells you when to reach for each; read the detail
200 only for the ones you are actually using, and never skip a capability just to avoid the read.
201 Phase 1 is
202 **design-only**: never emit page `.tsx` here. Each level is confirmed via `AskUserQuestion` before
203 the next begins, and `app-spec.json` is persisted after each — full prompts in the playbook.
204 - **Level (a0) — personas & jobs-to-be-done** (`personas[]`): **before proposing any tables**, ask
205 who will use the app and what each needs to get done. Jobs drive everything that follows — a
206 table exists because a job needs its data, a surface because a job needs to act on it. Deriving
207 the data model first reliably misses surfaces. Privileges come later; capture the jobs now.
208 - **Level (a) — data model**: entities/columns/relationships **derived from those jobs**; run the
209 **early data-model lint** (catches e.g. the relationship-vs-lookup collision before forms are
210 authored on top).
211 - **Descriptions are part of authoring, not a cleanup pass.** Every table, column, view, chart,
212 form, dashboard and business rule takes an optional `description`, and you should **write one as
213 you create the artifact** — never as a backfill. A name says what a thing is called; a
214 description says what it is *for*, and it is the only intent an app carries that an agent can
215 read back later when it inspects an app it did not build. Describe the purpose, not the shape:
216 `"Severity 1-5; drives the escalation rule and the SLA clock"`, not `"The priority column"`.
217 (`commands[]` and `Customer` columns accept one but the SDK cannot write it — you'll get a
218 warning; `personas[]` does not take one at all. See the schema reference for why.)
219 The app itself takes two: `app.description` is the short tile text, and `app.aiDescription` is
220 the **routing description** an orchestrator reads to choose between apps — who the app is for,
221 what it covers and excludes, and how to tell it from a sibling app over the same tables. Write
222 it whenever such a sibling exists or is planned.
223 - **Level (b) — artifacts + page-intents + design**: **enumerate every surface each job needs and
224 classify it** per the genpage-first policy above — record CRUD → form + view; anything else
225 (overview/landing, dashboard, KPIs, analytics, guided/wizard flow, composite or comparison
226 screen) → a page **intent** (`source: { kind: "intent" }`). State each classification out loud;
227 if the app needs no pages, say so and why rather than silently omitting them. Then forms + views
228 + charts + sample data and the optional `design` contract, mapping each job to its surfaces via
229 `jobs[].surfaces[]`. **No page `.tsx` here.** `dashboards[]` only on explicit request.
230 - **Level (c) — access** (`personas[].jobs[].privileges[]`): personas and jobs already exist from
231 (a0) and the entities now exist, so **only add the privileges each job needs** — don't re-ask who
232 the users are. The builder unions them into one role per persona and grants the app to it so it
233 **opens for that persona, not just sysadmins**. **Render the roles + per-entity access as a table
234 in your chat reply** (the user can't approve an access model they can't see — see the CRITICAL
235 note above). If they want no roles, tell them jobs live in `personas[]` so dropping the roles
236 drops the recorded jobs too; offer read-only privileges to keep the record. (Column-level
237 security and access teams are not yet supported — see *Notes & limits*.)
238 - **Whole-app preview** (design gate for Level (b)): `node "${PLUGIN_ROOT}/scripts/preview-app.js" --spec @<working-dir>/app-spec.json`
239 renders data-model + sitemap + form wireframes + page-intents + design contract. **Reproduce the
240 ENTIRE rendered output verbatim in your chat reply, inside a fenced ` ``` ` code block — do NOT
241 leave it in the (collapsed, invisible) tool output, and do NOT just summarize "the preview looks
242 right" (see the CRITICAL note above).** The user must be able to SEE each form, the sitemap, and
243 the page intents they are approving. For a single form only: `node "${PLUGIN_ROOT}/scripts/preview-form.js" --spec @<working-dir>/app-spec.json`.
244 - **Don't pre-create tables/columns** — the build does it idempotently.
2455. **Guardrail lint (hard gate)** — run the **full** lint on the complete spec; **errors block**, warnings teach. If it blocks (or warns), **paste the findings into your chat reply** — tool output is collapsed and invisible to the user (see the CRITICAL note above), so the user can't fix what they can't see:
246 ```bash
247 node "${PLUGIN_ROOT}/scripts/lint-app-spec.js" --spec @<working-dir>/app-spec.json --json
248 ```
249 This runs migration and `validateAppSpec` — the gates the build itself runs on load — plus the `lintAppSpec` authoring guardrails, which the builder does **not** run. It exits non-zero on errors. It validates under the `plan` profile (which allows not-yet-generated intent pages), matching Step 6's dry run.
2506. **Plan-mode approval (the single build approval)** — present the plan **including the build
251 dry-run's phase-grouped plan** (run `build-model-app.js` without `--apply`, using the `plan`
252 profile that allows intent pages) inside `EnterPlanMode`, then `ExitPlanMode` to get the user's
253 go-ahead. On approval, **Phase 1.5** (generate-pages) runs first, then **Phase 2** applies
254 directly (no second dry-run/go-ahead). Render the design document — never hand-write it:
255 ```bash
256 node "${PLUGIN_ROOT}/scripts/write-app-spec-doc.js" --spec @<working-dir>/app-spec.json --env <envUrl>
257 ```
258 It writes `<working-dir>/model-app-plan.md` (jobs → surfaces traceability, data model, every
259 surface, navigation, access model, sample data) and prints `{ ok, docPath, bytes, warnings }`.
260 **Surface those `warnings`** — they name design gaps such as a job with no covering surface or an
261 app with no generative pages. Tell the user where the document is; it's theirs to keep, and it's
262 regenerable after any spec edit.
263 
264### Phase 1.5 — Generate pages (main loop, headless workers)
265 
266After plan-mode approval (before the full build):
267 
2681. **Data pre-build** — schema-only build so `generate-types` can resolve real column names:
269 ```bash
270 node "${PLUGIN_ROOT}/scripts/build-model-app.js" \
271 --env <envUrl> --spec @<working-dir>/app-spec.json --stage data --apply
272 ```
273 `--stage data` applies solution + data-model only — **no `--sample-data`** (rows are created
274 once in the full build). Only `--stage data` is apply-safe; all other `--stage` selectors
275 and legacy `--from/--to/--only/--skip` selectors are dry-run inspection only.
276 
2772. **Types** — generate Dataverse type bindings for the entities the pages read:
278 ```bash
279 pac model genpage generate-types --data-sources "<entity1,entity2,…>" --output-file <working-dir>/RuntimeTypes.ts
280 ```
281 `--data-sources` is the union of every intent page's `dataSources`. Skip this step entirely when
282 every intent page is mock-only. On Windows use forward slashes in the path.
283 
2843. **Page plan (adapter)** — the page worker's input contract is a *plan document*, not an App Spec,
285 so project the spec into one. This also echoes the per-page dispatch parameters:
286 ```bash
287 node "${PLUGIN_ROOT}/scripts/write-page-plan.js" \
288 --spec @<working-dir>/app-spec.json --working-dir <working-dir> --env <envUrl> \
289 --app "<app name>" --languages "<languages from the environment probe>"
290 ```
291 It writes `<working-dir>/app-builder-page-plan.md` and prints
292 `{ ok, planPath, pages: [{ name, key, file, dataMode, intent }] }`. Pass `--languages` through
293 from the environment probe — omitting it silently defaults every plan to English-only and drops
294 the localization pattern. The command fails (before writing) if the plan would name a sample that
295 doesn't exist, or if a page a worker will write is not a safe target in the working directory — a
296 link at its path, a folder that links outside the directory, or a page already there under another
297 spelling — or if a link, a hard link or a folder is already at the plan's own path. Stop and fix
298 what it names; do not dispatch any worker.
299 
3004. **Generate** — for each page from step 3 with `intent: true`, dispatch the **headless**
301 `genpage-page-builder` worker via `Task`. Use its documented input contract verbatim — a missing
302 field is why a page silently never becomes `.tsx`:
303 
304 > You are the genpage-page-builder agent. Generate the **[name from step 3]** page.
305 >
306 > - Target file: [file from step 3 — already includes .tsx; do NOT append another]
307 > - Plan document: [absolute path to the app-builder-page-plan.md written in step 3]
308 > - Data mode: **[dataMode from step 3 — `dataverse` or `mock`]**
309 > - Connectors: **none**
310 > - Telemetry: **disabled**
311 > - RuntimeTypes: [absolute path to RuntimeTypes.ts] ← omit this line when Data mode is `mock`
312 > - Working directory: [absolute working-dir path]
313 > - Plugin root: ${PLUGIN_ROOT}
314 >
315 > Follow the instructions in your agent file. Write [file] and return your result when done.
316 
317 The plan's `## Environment` carries `Mode: app-builder` and every page row carries a **Key**, so
318 the worker emits `"PAGEREF_<key>"` for cross-page navigation (never a file-derived token — a
319 downloaded page's `codeFile` is a path, not its identity). Custom nav ids go in `data:` — never
320 `recordId`. `Connectors: none` and `Telemetry: disabled` are constants here: the App Spec has no
321 connector-binding concept (so the projected plan always says `No connector bindings.`), and
322 `/app-builder` never runs the Phase 4.7 `custom-telemetry` probe. Both are stated explicitly
323 rather than omitted — the page-builder treats a missing line as the same fail-closed value, but
324 an explicit line is what makes the dispatch contract checkable.
325 
3265. **Validate + commit the transition (transactional)** — never flip `source` by hand, and never
327 flip pages one at a time as workers return. Run:
328 ```bash
329 node "${PLUGIN_ROOT}/scripts/promote-intent-pages.js" \
330 --spec @<working-dir>/app-spec.json --working-dir <working-dir>
331 ```
332 It checks every generated page (file written, structurally a module, `PAGEREF_` tokens
333 canonical and in exact parity with the spec's `navigatesTo` edges) and only then flips **all**
334 of them `intent → { kind: "tsx", codeFile }` in a single atomic write. On any failure it
335 **exits 3 and leaves `app-spec.json` untouched**, printing which page failed and why —
336 regenerate just those pages and re-run. All-or-nothing on purpose: a half-flipped spec would
337 claim a `.tsx` that was never written, and Phase 2's `deploy` profile fails fast on any
338 remaining `source.kind === "intent"`.
339 
3406. Proceed to **Phase 2** (full idempotent build).
341 
342> ⚠️ The interactive author **never** runs inside a `Task` subagent. Only pure, headless
343> code-gen workers are dispatched here — all user-facing prompts originate in the main loop.
344 
345### Phase 2 — Build (narrated, main loop)
346 
347> **Always use `scripts/build-model-app.js`. Never hand-write a builder.** It's idempotent
348> (skips existing solution/tables/columns/relationships — so new, existing, and mixed envs all
349> just work), so you don't pre-create anything or special-case existing tables.
350 
351**The build plan was already presented and approved in plan mode** (Phase 1 Step 6 shows the engine's
352real dry-run plan), so on approval **apply directly** — one build approval, no second go-ahead. Add
353`--verify` so the build self-checks after applying (see Phase 3):
354 
355```bash
356node "${PLUGIN_ROOT}/scripts/build-model-app.js" \
357 --env <envUrl> --spec @<working-dir>/app-spec.json --apply --verify [--sample-data] [--publish]
358```
359 
360Each step streams its status live (`[n/total] ✓ created` / `⊘ skipped` / `✗ failed — <error>`) and a
361closing `✓ build complete — X created, Y skipped, Z failed` summary.
362 
363> **Keep the build's progress visible.** A full build runs for several minutes. Let its output
364> **stream** — do NOT pipe it through `Select-Object -First/-Last N` or `Select-String` head-limits,
365> which buffer and hide progress until the run ends (and can truncate a still-running pipe). To
366> capture the log use `Tee-Object -FilePath <log>` (no head-limit). The build also prints a
367> `▸ live progress:` line pointing at `<workspace>/.maker-workspace/build-status.json` — a snapshot
368> (`state`, `steps`, `lastPhase`, `lastLabel`) overwritten every step. Read it (or tail
369> `build-log.jsonl`) any time to report where a long build is, even if stdout is buffered.
370 
371(**Reaching Phase 2 without a fresh plan-mode approval** — resuming a failed build, or a quick edit
372re-run — do a **dry-run first** (drop `--apply`), **paste the phase-grouped plan verbatim into your
373chat reply** (tool output is collapsed — see the CRITICAL note above), and get a go-ahead
374before applying.)
375 
376**Stage selector (`--stage <data|ui|app|publish>`)** maps to its phase range. On `--apply`, ONLY
377`--stage data` is accepted (solution + data-model, no rows in run 1; run 2 is a full build). All
378other stages and the legacy `--from/--to/--only/--skip` selectors are dry-run inspection only —
379their phase ranges are not dependency-closed and are rejected on `--apply`.
380 
381**Language (`--language-code` / `--languageCode <lcid>`)** — you normally do **not** pass this.
382Data-model labels are
383stamped with the organization's own base language, read once per build. Pass it only to author
384labels in a different **provisioned** language, or if the build warns that it could not determine
385the base language and fell back to 1033.
386 
387If you pass an LCID the organization has **not** provisioned, the build now stops in the data-model
388phase, before any label is written, and lists the ones it does have. That check exists because
389Dataverse handles this **inconsistently**: table and choice labels are accepted (HTTP 204) and
390silently stored under the organization's base language, while `DateTime` and `Memo` columns are
391rejected with *"The language code N is not a valid language for this organization"*. Without the
392check a build therefore creates the table with the wrong labels and then dies partway through the
393columns, phases away from the flag that caused it — which reads like an environment fault. List the
394provisioned set with
395`node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET RetrieveProvisionedLanguages`.
396The check is best-effort: if that read fails the build proceeds unchanged.
397 
398The App Spec field `languageCode` pins the same value across runs. It now covers the **whole** build:
399data-model labels, and form, dashboard and sitemap labels — the SDK serializers that used to hardcode
4001033 take the authoring language as an option.
401 
402Narrate progress as it runs. Transient env errors (429 customization-lock, 503 SQL-timeout, a SQL
403deadlock, concurrent-op guards) are **auto-retried** with backoff on `--apply` (the build is idempotent, so a
404retry reuses what's already created). If the build still **halts** (`BuildHalt`) on an
405unrecoverable error, surface it and ask the user how to proceed via `AskUserQuestion` (adjust the
406spec / cancel), then re-run. Everything is scoped to a dedicated unmanaged solution; **`--publish`
407gates the final *bulk* publish** — edit/finalize paths still publish their one targeted artifact so the
408change takes effect (see *Notes & limits*).
409 
410**Recovery from a failed or halted build: run the full build again.** The build is idempotent — every
411phase re-uses what's already created and only fills the gaps. SDK metadata is persisted under
412`<working-dir>/.maker-workspace/` (override with `--workspace`). There is no apply-safe
413`--from <phase>` shortcut; a full rerun is the correct and safe recovery path. One exception: an
414`already-exists` halt recurs on a rerun until that workspace directory is deleted — follow its message.
415 
416### Phase 3 — Verify & iterate
417**`--apply --verify` already reconciled the spec against what deployed** (Phase 2) — the build appends a
418`verify PASS` / `verify FAIL — N missing` line and **exits non-zero on a silent partial** (an artifact
419created but not wired, or a phase that quietly produced nothing). To **re-check later** — e.g. after a
420Maker change, without rebuilding — run the standalone read-only verifier (entities/columns/views/charts/
421forms and sitemap subareas + icons; exits non-zero and lists anything missing):
422 
423```bash
424node "${PLUGIN_ROOT}/scripts/verify-model-app.js" --env <envUrl> --spec @<working-dir>/app-spec.json
425```
426 
427Then open the app in the browser. Refine `app-spec.json` and re-run Phase 2 to iterate.
428 
429**Teardown (cleanup).** To remove everything an App Spec built — e.g. a live-verification probe or a
430failed build — run the classifier-safe teardown. It deletes only the artifacts the spec declares, in
431dependency order (**app module → dashboards → command bars → forms → security roles → charts → views
432→ reset enriched default views to drop parent lookups → relationships → AI row summaries → tables
433[children-first] → web resources (generated app icon + page manifest + declared) → global choices →
434solution**). Forms/charts/views/relationships are deleted **explicitly before tables** (a table
435delete does not reliably cascade cross-references; it does remove the table's own columns). Security
436roles come **after forms** because a form assigned to a role names it inside its own formxml, which
437the platform counts as a dependency, and **before** relationships/tables because a role holding a
438table's privileges can block that table's delete. Command
439teardown removes the whole command bar for any entity the spec authored commands on. **Teardown only
440deletes tables this build created** — a **system/standard table** (account, contact, …) is
441auto-detected and **skipped**, and a **reused custom table** is skipped when its entity is flagged
442`"existing": true`, so pre-existing data survives. The same flag protects **relationships and global
443choices** — a download sets it on every one it recovers, so tearing down a downloaded spec never removes
444a lookup column from a retained table or deletes a shared option set. Dashboards are found by name, so
445teardown deletes only those the app's solution holds, never another app's namesake — and none when the spec
446has no real solution to ask (the `Default` a download may leave); it keeps the solution itself while any step
447failed, so a re-run can still tell, and keeps the page manifest while the generative-pages step failed, so a
448re-run still finds the pages the app authored. **Dry-run by default**; add `--apply
449--allow-destructive` to actually delete (`--clear-workspace` also prunes `.maker-workspace/`).
450**`--allow-destructive` is required for `teardown --apply`** — without it teardown refuses and
451touches nothing.
452 
453```bash
454node "${PLUGIN_ROOT}/scripts/teardown-model-app.js" \
455 --env <envUrl> --spec @<working-dir>/app-spec.json [--apply] [--allow-destructive] [--clear-workspace]
456```
457 
458**Safety flags (build + teardown).** The apply path is fail-closed against destructive operations:
459 
460- **`--allow-destructive`** — authorize destructive operations. For `build --apply`: authorizes
461 overwriting an existing app in unattended mode, and allows explicit-layout form-field removals or
462 sitemap-target drops; also authorizes DETACHING a `pages-removed` page's nav subarea (the page
463 record is left deployed — it is not deleted). For form and sitemap removals, the authority covers
464 the removals the last refusal or approved run recorded; if live state would lose anything more,
465 the apply halts, lists only the newly destructive removals, and records that new list for review.
466 A field or sitemap target that appears during a run is kept or halted before push, never removed
467 unseen. The record is consumed only after a full successful build that ran the removal phases and
468 kept nothing; it is kept on failures, partial runs, changed-only runs, unreadable records, and
469 failed discovery. For `teardown --apply`: **required** — all deletes
470 are destructive by construction, so teardown without this flag halts before touching anything.
471- **Pages-phase safety HALTs.** The build halts on identity or safety violations rather than
472 proceeding with potentially wrong state. Surface the HALT reason and follow the recovery hint:
473 - `pages-identity-conflict` — spec `pageId` and manifest disagree on a key, or a duplicate id
474 spans two keys. Resolve the conflict manually (re-download or delete the manifest).
475 - `pages-manifest-corrupt` — the manifest cannot be parsed (two keys map to the same id). Delete
476 the manifest web resource and rebuild from scratch.
477 - `pages-removed` — a live page was dropped from the spec. Re-add it, or pass `--allow-destructive`
478 to detach it from the nav (the page record stays deployed; rebuild finalizes the sitemap).
479 - `pages-shared-across-apps` — the page appears in another app's sitemap. Detach it in Maker.
480 `--allow-destructive` does **not** bypass this halt.
481 - `pages-shared-check-failed` / `pages-existence-failed` / `pages-sitemap-read-failed` — a
482 prerequisite read failed; the build can't proceed safely. Retry on a transient error; check
483 permissions on a persistent one.
484- **`--non-interactive`** — suppress interactive prompts (for automation / CI). A non-interactive
485 build that encounters an existing app **fails** instead of warning-and-proceeding, unless
486 `--allow-destructive` is also set. Does **not** grant destructive authority on its own — only
487 `--allow-destructive` does.
488- **`POWER_PLATFORM_SKILLS_NONINTERACTIVE=1`** (or `true`) — env-var equivalent of
489 `--non-interactive`. Same semantics: suppresses prompts only, never authorizes destructive ops.
490 Set this in CI job environments to avoid interactive-prompt hangs.
491- In **autopilot / eval mode** (`--non-interactive` + `--allow-destructive`), `preview-app.js`
492 is written to disk as the design artifact before plan execution; interactive consent gates are
493 bypassed and the build is fail-closed against any destructive op not explicitly authorized.
494 
495### AI-first features
496 
497The `ai` block in the App Spec controls four app-level features and per-table Copilot row
498summaries. All features are **admin-gated**: they are enabled only where the environment
499administrator has turned them on in Power Platform Admin Center (Environments → Settings →
500Product → Features). The `ai-features` build phase preflights each setting via the SDK
501(`RetrieveSetting`) and, for anything off, **skips it with a warning** — it never fails the
502build and cannot flip admin or tenant switches itself.
503 
504**Preflight (standalone):**
505```bash
506node "${PLUGIN_ROOT}/scripts/ai-preflight.js" --env <envUrl> [--app <uniqueName>]
507```
508Prints each feature's on/off status and the exact admin action required for anything that is off.
509Never fails.
510 
511**App-level features** (`ai.appFeatures`) — `formFill` (Copilot-assisted form fill), `nlSearch`
512(natural-language grid/view search), `nlChart` (NL chart / AI data visualization), `m365` (M365
513Copilot). All default to on except `m365`, which is left at its platform default; `false` writes an
514explicit Off for this app (each setting's values: `references/app-spec-schema.md` → `ai`).
515 
516**Per-table row summaries** (`ai.summaries`):
517- `default: "auto"` — the skill auto-selects good-candidate tables (skips lookup-only / config /
518 junction tables and the Dynamics 365-owned `incident`, `lead`, `opportunity`).
519- `default: "off"` — summaries disabled unless a table opts in explicitly.
520- Per-table overrides in `summaries.tables`: set `enabled`, a tailored `instruction`, and
521 `columns[]` (the fields the summary reads). A `{ "enabled": false }` entry opts a specific
522 table out.
523 
524**Prompt authoring guidelines** (for `instruction`): write for meaningful insights — not field/value
525repetition; never include record GUIDs; pull in recent activity where relevant; use
526audience-appropriate tone; aim for an explicit output shape (a short paragraph).
527 
528**Teardown** removes the AI records created by the `ai-features` phase (summary config rows and
529published AI models) in addition to the standard artifacts.
530 
531---
532 
533## Editing a deployed app (download → edit → rebuild)
534 
535The **same App Spec drives edit** — there is no separate edit path. When the user wants to change an
536existing app (add a field/view/page, edit a page's code, retitle/reorder nav, swap an icon), **pull the
537deployed app fresh into a spec first**, then edit that spec and re-run Phase 2:
538 
539```bash
540node "${PLUGIN_ROOT}/scripts/download-model-app.js" --env <envUrl> --app <appId|uniqueName|displayName> --out <working-dir>
541```
542 
543`--app` resolves in identity order: app id (GUID) → `uniquename` → display name. Prefer the **unique
544name** — it is immutable, while a display name can be renamed and is not unique. A display name that
545matches more than one app is refused, listing the candidate unique names rather than guessing.
546 
547This reconstructs the app into `<working-dir>/app-spec.json`. **Round-trip scope — be precise, it is
548not everything:**
549- **Round-trips:** the sitemap → `appShell` (all subareas + icons), **every** generative page (via
550 `pac model genpage download`; the pages are the sitemap's `GenPage` subareas, so Maker-added pages
551 are included, and each keeps its own name and model) into `pages[]` + their `.tsx`, the referenced
552 entities (minimal — the build reuses existing tables), **classic dashboards** (id-passthrough tiles
553 carrying the deployed view/chart ids), the icon web resources, and the solution.
554- **Does NOT round-trip:** `forms[]`, `views[]`, `charts[]`, `commands[]` — they come back empty.
555 All four **survive on the live app** (a rebuild preserves them by discovery), so a plain edit is
556 safe; they just aren't editable through the downloaded spec. Change them in Maker or a fresh spec.
557 
558Then:
559 
5601. **Always pull fresh at the start of an edit session** — someone may have changed the app in Maker. The
561 build reads an etag when it hydrates, so a write against an artifact changed since the pull throws a
562 version conflict → **re-pull and retry**, never clobber.
5632. Edit `app-spec.json` (and any page `.tsx`) for the requested change.
5643. **If the edit ADDS a page** (or resets a page's `source` back to `intent` to regenerate it),
565 **re-run Phase 1.5 before Phase 2** — a downloaded spec contains only `kind: "tsx"` pages, so an
566 added `intent` page has no code and Phase 2's `deploy` profile rejects it. Phase 1.5 is safe to
567 re-run: `write-page-plan.js` projects **all** pages (so the worker sees the real nav graph), a
568 worker is dispatched only where the echo says `intent: true`, and `promote-intent-pages.js`
569 leaves built pages untouched. Editing an *existing* page's `.tsx` by hand needs no regeneration.
5704. Re-run the **build** (Phase 2). It's idempotent: it reuses the existing app/tables/views, **updates
571 each page in place** (matched by name → `--page-id`, so no duplicates), and **preserves the
572 existing `GenPage` subareas** (the download enumerated them into `pages[]`/`appShell`, so the
573 full-replace sitemap write never drops them).
5745. **Verify** (Phase 3) to confirm only the intended change landed.
575 
576> **Prefer generative pages over classic dashboards** per the genpage-first policy. Classic dashboards
577> do round-trip (id-passthrough tiles), but the views and charts their tiles point at do not, so they
578> can only be edited in Maker.
579 
580## What the builder does (in order)
581 
582solution (idempotent) → data model — **discover** existing tables/columns/relationships via the SDK
583(`findTables` / `findColumns` / `fetchEntityMetadata`) and create only what's missing (`createTable`
584/ `createColumn` / `createRelationship`) → **sample data** (opt-in; relational/topological,
585`$parent`→`@odata.bind` using the entity-set name) → **web resources** (opt-in;
586`createWebResource` for form JS/HTML/CSS) → **views** → **charts** → **forms** (primary + columns
587laid out, explicit `tabs` honored; sub-grids, quick-views, and form JS (`events[]`) applied as
588canonical control cells / the `/bag/c` events region via the SDK's generic `addElement` surface)
589→ **business rules** (`businessRules[]` — authored as the workflow object model through the bound
590`CreateProcessWithWfomJson` member and activated; **skipped with a warning** on an environment that
591does not declare that member) →
592**business process flows** (`businessProcessFlows[]`; a category-4 process compiled from the
593authored stages and activated — this goes through the SDK's generic artifact surface, a plain
594`workflows` row, so it is NOT subject to the bound-member gate above) →
595**command bar** (`commands[]`) → **classic dashboards** (opt-in)
596→ **app module + sitemap** → **generative pages** (each page's `.tsx` was generated in Phase 1.5;
597the build uploads each `pages[]` page via `pac model genpage upload`, no `--add-to-sitemap`; then
598the SDK rewrites the sitemap once to add the `GenPage` subareas) → **AI features** (opt-in) →
599**security** (one role per `personas[]` entry, sized from its jobs' declared access; injects an
600app-module read privilege and associates the app to each role so it opens for that persona) →
601publish (opt-in). When the app has generative-page subareas the app module is created first WITHOUT
602them (they can't resolve until the pages upload), then the pages phase rewrites the sitemap. All
603Dataverse access goes through the SDK, so the downloaded metadata lands in `.maker-workspace/`.
604Independent ops (columns, views/charts/forms) run with bounded parallelism; publish is one
605round-trip per entity + the app. Views/charts build **before** forms so a sub-grid can reference the
606child view id. Each step emits `[n/total]`.
607 
608## Notes & limits
609 
610- **Headless, no browser.** The SDK (`cds-maker-sdk`, vendored at `scripts/vendor/`) generates
611 designer-grade FormXML/FetchXML/sitemap by reusing the designer's own serializers, and writes via
612 the Web API using an `az`-token HttpClient. No relay, no designer tab.
613- **Dedicated unmanaged solution per app** (review / teardown). **`--publish` gates the final
614 *bulk* publish** of the app's entity + app customizations (one `PublishXml` for every entity and the
615 app, which also carries the default-view enrichment's publish, deferred to it). It
616 does **not** suppress the small **targeted** publishes that edit/finalize paths must run so the change
617 takes effect — reconciling an existing form or view, wiring form events, placing quick-views,
618 re-syncing an existing app's sitemap, and finalizing the sitemap after generative pages each publish
619 that one artifact (an unpublished edit to a live artifact is invisible). A fresh build without
620 `--publish` still leaves new tables/columns/relationships staged-but-unpublished in the solution.
621- **Idempotent — but not full desired-state convergence.** Existing
622 solution/tables/columns/relationships/views/charts/forms/commands/dashboards are detected and
623 **reused**, so re-runs and existing-table envs work without collisions. **The caveat for EDITS:** a
624 rebuild never removes an artifact you dropped from the spec, and re-applies only part of an edit to
625 one that already exists. A **form** converges its fields (added; for an explicit layout, dropped ones
626 pruned — see `prune: false` below), column counts and spans. A **view** only *adds* columns: a
627 column removed from the spec, or a new column order, does **not** apply, and neither do its filters
628 or sort. A changed column type, and an edited command or dashboard, are not re-applied at all; an
629 existing chart takes only a changed description.
630 **To apply one of those edits, change it in Maker, or `teardown --apply` then rebuild fresh.**
631 `--verify` catches what is **missing** — a spec view column, filter condition or sort key (authored
632 sort keys must also keep their order), a relationship, command or dashboard — as a loud
633 `verify FAIL`. It tolerates **extra** deployed content, so a view column, filter or sort key you
634 removed from the spec still passes, just as one a maker added by hand does.
635- Not in scope (later): **conditional** command visibility (Power-Fx-only), **titled
636 command groups** (from-scratch — needs an SDK-synthesized parent row), lookup/associated views,
637 multi-area sitemaps, **column-level (field) security**, and **access teams / hierarchy security**
638 (both tracked SDK follow-ups). The security surface today is role-per-persona plus per-form role
639 assignment, and both of those ship. Also out of scope: the BPF knobs the spec gate rejects
640 (cross-entity stages, branching, stage actions).
641- **Environment-gated (may not work where you are running):**
642 - **Business rules** (`businessRules[]`). The SDK writes a rule through the bound
643 `CreateProcessWithWfomJson` member — the same one the modern business-rule designer uses — and
644 **has no fallback**: the client-side workflow-XAML compiler it used to fall back on was removed
645 upstream because it covered 4 of the 7 action types and a single clause, so it silently narrowed
646 a rule into something that did not say what the author wrote. Environments that do not declare
647 the member therefore **cannot host business rules at all** — and that is the common case, not an
648 edge case. The build **skips** the rules, warns once naming the
649 member, and builds everything else normally, so an app is never left half-created. If rules are
650 essential, verify the environment first.
651 **Business process flows are NOT affected by this gate** — a BPF is written as an ordinary
652 `workflows` row through the SDK's generic artifact surface, with no bound member involved.
653- Supported: the full data model — all column types, **AutoNumber primary**, global choices, status
654 reasons, alternate keys, **N:N + junction-with-payload**; **`required` reconciled on existing
655 columns** (an explicit `required` converges on rebuild; an omitted one never demotes);
656 **Boolean `defaultValue`**, whole-number **`integerFormat`** (incl. `Duration`), and per-column
657 **write permissions** (`isValidForCreate` / `isValidForUpdate` / `isValidForRead` — this is how you
658 make a column read-only), all applied on **create and on rebuild**; adaptive
659 main forms with **1:N / N:N sub-grids**; **per-field form control** (`readOnly` / `hidden` /
660 `after` positioning, plus `prune: false` to edit a subset of a form non-destructively);
661 **quick-create / quick-view forms** (`formType`) + **quick-view placement**
662 (`forms[].quickViews[]`); **per-form security roles** (`forms[].securityRoles` — name the
663 `personas[]` this form is offered to, or `everyone: true`; applied after the roles exist. A form
664 with no assignment is visible to **every** role, so this **restricts** a form rather than granting
665 it); **which form a table opens with** (`forms[].isDefault` / `entities[].mainFormOrder` set the
666 table's Main Form Set order, on existing tables too — a user's remembered form still opens first for
667 that user; see app-spec-schema.md); Choice-column charts; **business rules** (`businessRules[]` — authored as the modern workflow
668 object model and activated; see the environment gate above); **security roles** (`personas[]` — one role per
669 persona sized from its jobs-to-be-done, with app access so the app opens for non-admins);
670 **dashboards** (`dashboards[]` — chart/list/iframe/webresource tiles) + **dashboard sitemap
671 placement**; **generative pages** (`pages[]` — the genpage-first default, uploaded via
672 `pac model genpage upload` and surfaced as `GenPage` sitemap subareas; full **create + edit**
673 round-trip via `download-model-app.js`); **modern command-bar buttons** (`commands[]`) incl.
674 **flyout / split-button menus**; **rich view filters** (`eq-userid`/`this-week`/`in`/`not-in`);
675 **custom grid rendering** (`entities[].columns[].visualization` — radial dial / line chart /
676 heat map / star rating, preview); web resources + form JS event handlers; sample data with
677 **multi-parent `$parents`** + **`statusReason`**. See [`docs/app-builder-capabilities.md`](../../docs/app-builder-capabilities.md) and
678 [`references/app-spec-schema.md`](../../references/app-spec-schema.md) (plus
679 [`app-spec-schema-advanced.md`](../../references/app-spec-schema-advanced.md) for the conditional
680 fields it points at) — author from those docs; you should not need to read the SDK, lint, or
681 engine to write a spec.
682 

Discussion

Alternatives

Academy guideStop and check this skill before finishing any reply to a question about how to use Claude or a Claude product — it recommends matching courses, tutorials, and use cases from Claude Academy (academy.claude.com), Anthropic's learning hub. Trigger on: "how do I", "how can I", "getting started with", "what can Claude do", "teach me", "learn to use"; questions about artifacts, projects, skills, plugins, connectors, MCP; requests about rolling Claude out to a team, class, or organization; and any ask for training materials, onboarding content, or learning resources. Use it when the user is learning how to use a feature or product — not when they are mid-task and just want the task done. This skill composes with other skills: after consulting product documentation to answer how a Claude feature works, also check here for a matching course or tutorial — a docs-grounded answer and an Academy recommendation belong together. Only recommend on a strong match; never invent Academy content.Business & ops · Apache-2.0Analyze feature requestsAnalyze and prioritize a list of feature requests by theme, strategic alignment, impact, effort, and risk. Use when reviewing customer feature requests, triaging a backlog, or making prioritization decisions. · MITAdvocacy program designerUse when the user asks to "design an employee advocacy program", "set up founder-led sharing", or "build a share kit for the team"; produces an advocacy program blueprint in two modes — participation-driven opt-in (default) or top-down assigned with its coercion and authenticity risks flagged — with a voluntary opt-in roster spec submitted as channel-registry proposal events, share kits with mandatory per-person variation, staggered human posting windows plus anti-pod guardrails (no coordinated identical reshares, no engagement rings), per-person material-connection disclosure lines per FTC and 《互联网广告管理办法》, and a Slack/Teams distribution spec. Not for paid creator campaigns — use campaign-planner. 员工倡导/创始人IP分享/内部分享计划/披露合规Business & ops · Apache-2.0Act as a product managerGuides the AI to act as a product manager, assisting in writing product requirement documents and addressing product-related queries.Business & ops · CC0-1.0