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
⚠️ Preview. This skill is in preview — its App Spec shape, flags, and build behavior may change
between versions. Review the plan-mode summary before applying, and prefer a non-production
environment while it stabilizes.
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.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. v1 is single-entity and linear: cross-entity stages, branching, stage
actions and security-role grants 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
- Derive a short kebab-case slug from
$ARGUMENTS (e.g. "Project Tracker" → project-tracker).
- 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:
- Prereqs —
node --version, pac help (≥ 2.7.0).
- 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).
- Detect existing —
pac model list-tables --search … (exact-match) and pac model list
to find tables/apps already present; build around them.
- 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. 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.)
- 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.
- Guardrail lint (hard gate) — run the full
spec-lint.js 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 -e "const{lintAppSpec}=require('${PLUGIN_ROOT}/scripts/lib/spec-lint.js');const s=require('<working-dir>/app-spec.json');const r=lintAppSpec(s);console.log(JSON.stringify(r,null,2));process.exit(r.ok?0:1)"
- 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):
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.
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.
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.
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: 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: disabled is a constant here: the App Spec has no connector-binding
concept, so the projected plan always says No connector bindings.
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".
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 (#455).
Narrate progress as it runs. Transient env errors (429 customization-lock, 503 SQL-timeout,
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.
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. 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 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 true except m365; set any to false to opt out.
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; names come from the sitemap's GenPage subarea titles, so
Maker-added pages are included) 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:
- 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.
- Edit
app-spec.json (and any page .tsx) for the requested change.
- 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.
- 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).
- 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 (a PublishXml per entity + the app). 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 ADDITIVE, not yet 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
…(truncated)
1---2name: app-builder3description: (Preview) 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.4---56> **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.78# app-builder — intent → model-driven app910> ⚠️ **Preview.** This skill is in preview — its App Spec shape, flags, and build behavior may change11> between versions. Review the plan-mode summary before applying, and prefer a non-production12> environment while it stabilizes.1314Turn a natural-language intent into a deployed model-driven app. You author a reviewable **App Spec**15(JSON) with the user across confirmed turns, then a deterministic engine (`cds-maker-sdk`, vendored)16builds it — tables/columns/relationships, sample data, views, Choice-column charts, adaptive forms17with sub-grids, **generative pages** for overview/dashboard surfaces, and the app module + sitemap.18The **same spec drives create and edit**: download a deployed app back into a spec, change it, and19re-run the build (it's idempotent).2021## CRITICAL — run the interactive flow in THIS conversation (the main loop)2223> **You MUST run the authoring questions and the build narration yourself, in the main24> conversation. Do NOT dispatch a subagent (`Task`) for the interactive steps.**25>26> A subagent is headless — `AskUserQuestion` and plan mode do not reach the user from inside one27> (its only output is its final message). The whole point of this skill is the multi-turn,28> propose-then-confirm experience, so every `AskUserQuestion`, `EnterPlanMode`, and live build29> status line must originate here, in the main loop.3031## CRITICAL — the user sees your chat message, NOT tool output3233> **Shell/tool output — the result of running `preview-app.js`, a dry-run plan, or a lint —34> is COLLAPSED BY DEFAULT in the UI. The user does NOT see it unless they manually expand the35> tool panel.** Running the command is therefore NOT the same as showing the user. Every artifact36> the user must **read, review, or approve** — the whole-app **preview wireframes**, the37> **dry-run build plan**, and blocking **lint findings** — MUST be reproduced **verbatim in your38> chat reply**, inside a fenced ` ``` ` code block. Never say "the preview looks right" and leave39> the content buried in a collapsed panel: **paste it into your message.** This is the #1 cause of40> "the wireframes aren't visible" — the preview ran, but its output stayed hidden.4142## Capabilities — the full toolbox (pick best-fit per requirement)4344You are a **complete** model-driven app builder, not a single-surface tool. Everything below ships in45one App Spec and one build — choose what best serves the user's requirement to make a **useful,46prod-ready** app; don't under-build (a bare table list) or over-build (surfaces nobody asked for):4748- **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 data49- **Record UI** — forms (sub-grids, quick-create / quick-view), views (with enriched default columns), charts50- **Custom grid rendering** (preview) — `entities[].columns[].visualization`: render a column as a51 `RadialDial`, `LineChart`, `HeatMap` or `StarRating` in **every** grid and view that shows it,52 instead of plain text. Reach for it when a column is a *magnitude a user scans* (a score, a53 utilization %, a rating, a priority) rather than a value they read exactly — it makes a list54 scannable at a glance for one line of spec. It is per-*column*, so set it once on the column, not55 on each view. **Preview:** on an environment where it is not provisioned the build skips it and56 everything else still deploys, so it is always safe to include.57- **Actions** — modern command-bar buttons (incl. flyout / split menus), web resources (form JS / HTML / CSS).58 Two rules when writing that JS, both learned from buttons that deployed perfectly and then did59 nothing: a command handler is handed the record (`function doThing(primaryControl)`) — the build60 supplies the parameter, so write that signature; and **never hardcode Choice values** like61 `100000003`, because they are assigned per publisher. Resolve by label via `getOptions()`62 (see `references/app-spec-schema.md` → webResources). Note also that command and web-resource63 **edits do not redeploy on rebuild** — the phases reuse what exists, so changing a button or a64 script means deleting it first.65- **Form logic without code** — `businessRules[]`: show/hide, lock/unlock, set-required and66 set-value, driven by a condition on the record. **Reach for a business rule before form JS** when67 the requirement is field-level and declarative — it is visible in the maker, survives solution68 export, and needs no web resource. Use form JS when the logic needs a real API call, cross-record69 work, or anything beyond the four supported actions.70- **Guided processes** — `businessProcessFlows[]`: the staged bar across the top of a record71 (ordered stages, each with steps bound to that table's columns — **every step must bind a `field`**;72 the platform rejects one without, so use a Boolean flag for a manual check-off). Reach for one when73 the user describes work moving through **phases** — "triage →74 investigate → resolve", "lead → qualify → close". Author it `Active` (the default) or the stage bar75 does not appear at all. v1 is single-entity and linear: cross-entity stages, branching, stage76 actions and security-role grants are **rejected** by the spec gate, so offer Maker for those rather77 than writing them into the spec.78- **Surfaces** — **generative pages** (modern dashboards / overviews / analytics / landing — the default),79 classic dashboards (opt-in), external URLs80- **App shell** — the app module + sitemap, with per-subarea icons. Turn on the **modern shell** with81 `app.newLook: true` unless the user asks for the classic one; it is opt-in and best-effort, so a82 tenant without the setting still gets a working app. `app.headerNavigationRefresh` controls the83 **Wave 2 header/navigation refresh** — a *separate, independent* setting whose platform default is84 **ON**, so set it to `false` only when the user explicitly wants the classic header.85- **Security & access** — one **security role per persona**, sized from that persona's jobs-to-be-done86 (the entity access each job needs, unioned into the role), so the app **opens for non-admins**.87- **AI-first features** (admin-gated) — form-fill assist, natural-language grid/view search, NL chart88 / AI data visualization, M365 Copilot (opt-in); per-table Copilot row summaries (Insight Cards)89 with tailored prompts, auto-selected for good-candidate tables9091Author the **smallest spec that fully satisfies the ask**, then let the user refine. The **Genpage-first92policy** below is the record-vs-dashboard rule; [`references/app-spec-schema.md`](../../references/app-spec-schema.md)93documents every field.9495## Genpage-first policy (surface classification)9697Every app surface is one of two kinds — **enumerate the app's surfaces and classify each one**, don't98decide page-by-page in passing:99100- **Record surfaces** (create/read/update/list a table's rows) → a **model-driven form + view**.101- **Everything else** — overview/landing, dashboard, KPIs, analytics, guided or wizard flow,102 composite or comparison screen → a **generative page** (`pages[]`), **not** a classic dashboard.103104Rules:105106- An app whose jobs include an overview, a queue, analytics or a guided flow but which proposes no107 pages has missed a surface. Where the app genuinely is record-CRUD only, say so explicitly rather108 than silently omitting pages.109- A **traditional `dashboards[]`** is emitted **only on explicit request** (e.g. "use a classic dashboard").110- A generative page is authored as a **design intent** (`source: { kind: "intent" }`,111 `schemaVersion: 2`) during Phase 1 — its `.tsx` is written in **Phase 1.5 — Generate pages**, after112 plan approval and after the data pre-build creates the tables so `pac model genpage generate-types`113 can emit `RuntimeTypes.ts`. See [`references/authoring-flow.md`](../../references/authoring-flow.md) → *Pages*.114- The build's `pages` phase uploads each page via `pac model genpage upload` **without115 `--add-to-sitemap`** — the SDK is the **single sitemap writer**, so a page's nav entry comes from a116 `page` subarea in `appShell` (referenced by the page's **`key`**). See117 [`references/app-spec-schema.md`](../../references/app-spec-schema.md) → `pages[]`.118- **Every page in `pages[]` must be sitemap-placed** — validation rejects any page absent from the119 sitemap. A "detail" page that receives a caller-supplied id is a normal sitemap page; it reads its120 input via `pageInput?.data?.<field>`. Navigation-only (headless) pages are not supported.121- **A page that declares `pageInput` MUST declare `directEntry`.** Because every page is122 sitemap-placed, a detail page is also reachable straight from the app navigation with **no input**123 — a state a user reaches by clicking. Say what happens then: `{ "behavior": "selector" }` shows a124 picker and then the record, `{ "behavior": "emptyState" }` explains and renders nothing broken.125 Prefer `selector` when the table is browsable; it is the more useful landing. Every key in126 `pageInput.data` must also be produced by some page's `navigatesTo[].data`, or the generated page127 reads a key nothing ever sets.128- **Three-authority page identity** (build + download + verify all follow this): (1) **IDENTITY** —129 the durable `<app>_pagemanifest` (`key → pageId`); a downloaded spec's own `pages[].pageId`130 outranks it for that rebuild. (2) **EXISTENCE** — env-wide `pac model genpage list` (crash-safe;131 decides create-vs-reuse). (3) **MEMBERSHIP** — the app's sitemap `GenPageId` set (placement,132 download enumeration, verify coverage). All matching is by id — never by display name.133- **Multi-page navigation uses `PAGEREF_<key>`** (the stable `pages[].key`) as the `pageId`134 placeholder; the build resolves each to the real page GUID in a run-scoped staging copy (the135 canonical `.tsx` is never mutated), then **verifies every nav edge** resolves to the deployed136 page's `GenPageId`.137138## Workflow139140### Phase 0 — Working directory1411. Derive a short kebab-case slug from `$ARGUMENTS` (e.g. "Project Tracker" → `project-tracker`).1422. Create the directory (`mkdir -p <slug>` on bash/PowerShell; `mkdir <slug>` on cmd, which has no143 `-p` and errors if it exists) and resolve its absolute path. It holds `app-spec.json`,144 `model-app-plan.md`, and `workflow-log.md`.145146### Phase 1 — Author the App Spec (interactive, main loop)147148Follow **[references/authoring-flow.md](../../references/authoring-flow.md)** step by step, running149every prompt yourself via `AskUserQuestion`. In short:1501511. **Prereqs** — `node --version`, `pac help` (≥ 2.7.0).1522. **Environment (PAC)** — `pac auth list`. If exactly one / an active profile, **confirm it153 (FYI), don't ask**. If several and none active, **ask** which to use. If none, ask the user154 to `pac auth create`. Capture the org URL (`pac org who`).1553. **Detect existing** — `pac model list-tables --search …` (exact-match) and `pac model list`156 to find tables/apps already present; build *around* them.1574. **Levelled authoring** — **first read the App Spec format** so you author to the exact158 shape (do this once; don't go spelunking through scripts):159 [`references/app-spec-schema.md`](../../references/app-spec-schema.md) and the worked sample160 [`samples/app-spec.support-desk.json`](../../samples/app-spec.support-desk.json). Phase 1 is161 **design-only**: never emit page `.tsx` here. Each level is confirmed via `AskUserQuestion` before162 the next begins, and `app-spec.json` is persisted after each — full prompts in the playbook.163 - **Level (a0) — personas & jobs-to-be-done** (`personas[]`): **before proposing any tables**, ask164 who will use the app and what each needs to get done. Jobs drive everything that follows — a165 table exists because a job needs its data, a surface because a job needs to act on it. Deriving166 the data model first reliably misses surfaces. Privileges come later; capture the jobs now.167 - **Level (a) — data model**: entities/columns/relationships **derived from those jobs**; run the168 **early data-model lint** (catches e.g. the relationship-vs-lookup collision before forms are169 authored on top).170 - **Descriptions are part of authoring, not a cleanup pass.** Every table, column, view, chart,171 form, dashboard and business rule takes an optional `description`, and you should **write one as172 you create the artifact** — never as a backfill. A name says what a thing is called; a173 description says what it is *for*, and it is the only intent an app carries that an agent can174 read back later when it inspects an app it did not build. Describe the purpose, not the shape:175 `"Severity 1-5; drives the escalation rule and the SLA clock"`, not `"The priority column"`.176 (`commands[]` and `Customer` columns accept one but the SDK cannot write it — you'll get a177 warning; `personas[]` does not take one at all. See the schema reference for why.)178 - **Level (b) — artifacts + page-intents + design**: **enumerate every surface each job needs and179 classify it** per the genpage-first policy above — record CRUD → form + view; anything else180 (overview/landing, dashboard, KPIs, analytics, guided/wizard flow, composite or comparison181 screen) → a page **intent** (`source: { kind: "intent" }`). State each classification out loud;182 if the app needs no pages, say so and why rather than silently omitting them. Then forms + views183 + charts + sample data and the optional `design` contract, mapping each job to its surfaces via184 `jobs[].surfaces[]`. **No page `.tsx` here.** `dashboards[]` only on explicit request.185 - **Level (c) — access** (`personas[].jobs[].privileges[]`): personas and jobs already exist from186 (a0) and the entities now exist, so **only add the privileges each job needs** — don't re-ask who187 the users are. The builder unions them into one role per persona and grants the app to it so it188 **opens for that persona, not just sysadmins**. **Render the roles + per-entity access as a table189 in your chat reply** (the user can't approve an access model they can't see — see the CRITICAL190 note above). If they want no roles, tell them jobs live in `personas[]` so dropping the roles191 drops the recorded jobs too; offer read-only privileges to keep the record. (Column-level192 security and access teams are not yet supported — see *Notes & limits*.)193 - **Whole-app preview** (design gate for Level (b)): `node "${PLUGIN_ROOT}/scripts/preview-app.js" --spec @<working-dir>/app-spec.json`194 renders data-model + sitemap + form wireframes + page-intents + design contract. **Reproduce the195 ENTIRE rendered output verbatim in your chat reply, inside a fenced ` ``` ` code block — do NOT196 leave it in the (collapsed, invisible) tool output, and do NOT just summarize "the preview looks197 right" (see the CRITICAL note above).** The user must be able to SEE each form, the sitemap, and198 the page intents they are approving. For a single form only: `node "${PLUGIN_ROOT}/scripts/preview-form.js" --spec @<working-dir>/app-spec.json`.199 - **Don't pre-create tables/columns** — the build does it idempotently.2005. **Guardrail lint (hard gate)** — run the **full** `spec-lint.js` 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:201 ```bash202 node -e "const{lintAppSpec}=require('${PLUGIN_ROOT}/scripts/lib/spec-lint.js');const s=require('<working-dir>/app-spec.json');const r=lintAppSpec(s);console.log(JSON.stringify(r,null,2));process.exit(r.ok?0:1)"203 ```2046. **Plan-mode approval (the single build approval)** — present the plan **including the build205 dry-run's phase-grouped plan** (run `build-model-app.js` without `--apply`, using the `plan`206 profile that allows intent pages) inside `EnterPlanMode`, then `ExitPlanMode` to get the user's207 go-ahead. On approval, **Phase 1.5** (generate-pages) runs first, then **Phase 2** applies208 directly (no second dry-run/go-ahead). Render the design document — never hand-write it:209 ```bash210 node "${PLUGIN_ROOT}/scripts/write-app-spec-doc.js" --spec @<working-dir>/app-spec.json --env <envUrl>211 ```212 It writes `<working-dir>/model-app-plan.md` (jobs → surfaces traceability, data model, every213 surface, navigation, access model, sample data) and prints `{ ok, docPath, bytes, warnings }`.214 **Surface those `warnings`** — they name design gaps such as a job with no covering surface or an215 app with no generative pages. Tell the user where the document is; it's theirs to keep, and it's216 regenerable after any spec edit.217218### Phase 1.5 — Generate pages (main loop, headless workers)219220After plan-mode approval (before the full build):2212221. **Data pre-build** — schema-only build so `generate-types` can resolve real column names:223 ```bash224 node "${PLUGIN_ROOT}/scripts/build-model-app.js" \225 --env <envUrl> --spec @<working-dir>/app-spec.json --stage data --apply226 ```227 `--stage data` applies solution + data-model only — **no `--sample-data`** (rows are created228 once in the full build). Only `--stage data` is apply-safe; all other `--stage` selectors229 and legacy `--from/--to/--only/--skip` selectors are dry-run inspection only.2302312. **Types** — generate Dataverse type bindings for the entities the pages read:232 ```bash233 pac model genpage generate-types --data-sources "<entity1,entity2,…>" --output-file <working-dir>/RuntimeTypes.ts234 ```235 `--data-sources` is the union of every intent page's `dataSources`. Skip this step entirely when236 every intent page is mock-only. On Windows use forward slashes in the path.2372383. **Page plan (adapter)** — the page worker's input contract is a *plan document*, not an App Spec,239 so project the spec into one. This also echoes the per-page dispatch parameters:240 ```bash241 node "${PLUGIN_ROOT}/scripts/write-page-plan.js" \242 --spec @<working-dir>/app-spec.json --working-dir <working-dir> --env <envUrl> \243 --app "<app name>" --languages "<languages from the environment probe>"244 ```245 It writes `<working-dir>/app-builder-page-plan.md` and prints246 `{ ok, planPath, pages: [{ name, key, file, dataMode, intent }] }`. Pass `--languages` through247 from the environment probe — omitting it silently defaults every plan to English-only and drops248 the localization pattern. The command fails (before writing) if the plan would name a sample that249 doesn't exist.2502514. **Generate** — for each page from step 3 with `intent: true`, dispatch the **headless**252 `genpage-page-builder` worker via `Task`. Use its documented input contract verbatim — a missing253 field is why a page silently never becomes `.tsx`:254255 > You are the genpage-page-builder agent. Generate the **[name from step 3]** page.256 >257 > - Target file: [file from step 3 — already includes .tsx; do NOT append another]258 > - Plan document: [absolute path to the app-builder-page-plan.md written in step 3]259 > - Data mode: **[dataMode from step 3 — `dataverse` or `mock`]**260 > - Connectors: **disabled**261 > - RuntimeTypes: [absolute path to RuntimeTypes.ts] ← omit this line when Data mode is `mock`262 > - Working directory: [absolute working-dir path]263 > - Plugin root: ${PLUGIN_ROOT}264 >265 > Follow the instructions in your agent file. Write [file] and return your result when done.266267 The plan's `## Environment` carries `Mode: app-builder` and every page row carries a **Key**, so268 the worker emits `"PAGEREF_<key>"` for cross-page navigation (never a file-derived token — a269 downloaded page's `codeFile` is a path, not its identity). Custom nav ids go in `data:` — never270 `recordId`. `Connectors: disabled` is a constant here: the App Spec has no connector-binding271 concept, so the projected plan always says `No connector bindings.`2722735. **Validate + commit the transition (transactional)** — never flip `source` by hand, and never274 flip pages one at a time as workers return. Run:275 ```bash276 node "${PLUGIN_ROOT}/scripts/promote-intent-pages.js" \277 --spec @<working-dir>/app-spec.json --working-dir <working-dir>278 ```279 It checks every generated page (file written, structurally a module, `PAGEREF_` tokens280 canonical and in exact parity with the spec's `navigatesTo` edges) and only then flips **all**281 of them `intent → { kind: "tsx", codeFile }` in a single atomic write. On any failure it282 **exits 3 and leaves `app-spec.json` untouched**, printing which page failed and why —283 regenerate just those pages and re-run. All-or-nothing on purpose: a half-flipped spec would284 claim a `.tsx` that was never written, and Phase 2's `deploy` profile fails fast on any285 remaining `source.kind === "intent"`.2862876. Proceed to **Phase 2** (full idempotent build).288289> ⚠️ The interactive author **never** runs inside a `Task` subagent. Only pure, headless290> code-gen workers are dispatched here — all user-facing prompts originate in the main loop.291292### Phase 2 — Build (narrated, main loop)293294> **Always use `scripts/build-model-app.js`. Never hand-write a builder.** It's idempotent295> (skips existing solution/tables/columns/relationships — so new, existing, and mixed envs all296> just work), so you don't pre-create anything or special-case existing tables.297298**The build plan was already presented and approved in plan mode** (Phase 1 Step 6 shows the engine's299real dry-run plan), so on approval **apply directly** — one build approval, no second go-ahead. Add300`--verify` so the build self-checks after applying (see Phase 3):301302```bash303node "${PLUGIN_ROOT}/scripts/build-model-app.js" \304 --env <envUrl> --spec @<working-dir>/app-spec.json --apply --verify [--sample-data] [--publish]305```306307Each step streams its status live (`[n/total] ✓ created` / `⊘ skipped` / `✗ failed — <error>`) and a308closing `✓ build complete — X created, Y skipped, Z failed` summary.309310> **Keep the build's progress visible.** A full build runs for several minutes. Let its output311> **stream** — do NOT pipe it through `Select-Object -First/-Last N` or `Select-String` head-limits,312> which buffer and hide progress until the run ends (and can truncate a still-running pipe). To313> capture the log use `Tee-Object -FilePath <log>` (no head-limit). The build also prints a314> `▸ live progress:` line pointing at `<workspace>/.maker-workspace/build-status.json` — a snapshot315> (`state`, `steps`, `lastPhase`, `lastLabel`) overwritten every step. Read it (or tail316> `build-log.jsonl`) any time to report where a long build is, even if stdout is buffered.317318(**Reaching Phase 2 without a fresh plan-mode approval** — resuming a failed build, or a quick edit319re-run — do a **dry-run first** (drop `--apply`), **paste the phase-grouped plan verbatim into your320chat reply** (tool output is collapsed — see the CRITICAL note above), and get a go-ahead321before applying.)322323**Stage selector (`--stage <data|ui|app|publish>`)** maps to its phase range. On `--apply`, ONLY324`--stage data` is accepted (solution + data-model, no rows in run 1; run 2 is a full build). All325other stages and the legacy `--from/--to/--only/--skip` selectors are dry-run inspection only —326their phase ranges are not dependency-closed and are rejected on `--apply`.327328**Language (`--language-code` / `--languageCode <lcid>`)** — you normally do **not** pass this.329Data-model labels are330stamped with the organization's own base language, read once per build. Pass it only to author331labels in a different **provisioned** language, or if the build warns that it could not determine332the base language and fell back to 1033.333334If you pass an LCID the organization has **not** provisioned, the build now stops in the data-model335phase, before any label is written, and lists the ones it does have. That check exists because336Dataverse handles this **inconsistently**: table and choice labels are accepted (HTTP 204) and337silently stored under the organization's base language, while `DateTime` and `Memo` columns are338rejected with *"The language code N is not a valid language for this organization"*. Without the339check a build therefore creates the table with the wrong labels and then dies partway through the340columns, phases away from the flag that caused it — which reads like an environment fault. List the341provisioned set with342`node "${PLUGIN_ROOT}/scripts/dataverse-request.js" <envUrl> GET RetrieveProvisionedLanguages`.343The check is best-effort: if that read fails the build proceeds unchanged.344345The App Spec field `languageCode` pins the same value across runs. It now covers the **whole** build:346data-model labels, and form, dashboard and sitemap labels — the SDK serializers that used to hardcode3471033 take the authoring language as an option ([#455](https://github.com/microsoft/power-platform-skills/issues/455)).348349Narrate progress as it runs. Transient env errors (429 customization-lock, 503 SQL-timeout,350concurrent-op guards) are **auto-retried** with backoff on `--apply` (the build is idempotent, so a351retry reuses what's already created). If the build still **halts** (`BuildHalt`) on an352unrecoverable error, surface it and ask the user how to proceed via `AskUserQuestion` (adjust the353spec / cancel), then re-run. Everything is scoped to a dedicated unmanaged solution; **`--publish`354gates the final *bulk* publish** — edit/finalize paths still publish their one targeted artifact so the355change takes effect (see *Notes & limits*).356357**Recovery from a failed or halted build: run the full build again.** The build is idempotent — every358phase re-uses what's already created and only fills the gaps. SDK metadata is persisted under359`<working-dir>/.maker-workspace/` (override with `--workspace`). There is no apply-safe360`--from <phase>` shortcut; a full rerun is the correct and safe recovery path.361362### Phase 3 — Verify & iterate363**`--apply --verify` already reconciled the spec against what deployed** (Phase 2) — the build appends a364`verify PASS` / `verify FAIL — N missing` line and **exits non-zero on a silent partial** (an artifact365created but not wired, or a phase that quietly produced nothing). To **re-check later** — e.g. after a366Maker change, without rebuilding — run the standalone read-only verifier (entities/columns/views/charts/367forms and sitemap subareas + icons; exits non-zero and lists anything missing):368369```bash370node "${PLUGIN_ROOT}/scripts/verify-model-app.js" --env <envUrl> --spec @<working-dir>/app-spec.json371```372373Then open the app in the browser. Refine `app-spec.json` and re-run Phase 2 to iterate.374375**Teardown (cleanup).** To remove everything an App Spec built — e.g. a live-verification probe or a376failed build — run the classifier-safe teardown. It deletes only the artifacts the spec declares, in377dependency order (**app module → dashboards → command bars → forms → security roles → charts → views378→ reset enriched default views to drop parent lookups → relationships → AI row summaries → tables379[children-first] → web resources (generated app icon + page manifest + declared) → global choices →380solution**). Forms/charts/views/relationships are deleted **explicitly before tables** (a table381delete does not reliably cascade cross-references; it does remove the table's own columns). Security382roles come **after forms** because a form assigned to a role names it inside its own formxml, which383the platform counts as a dependency, and **before** relationships/tables because a role holding a384table's privileges can block that table's delete. Command385teardown removes the whole command bar for any entity the spec authored commands on. **Teardown only386deletes tables this build created** — a **system/standard table** (account, contact, …) is387auto-detected and **skipped**, and a **reused custom table** is skipped when its entity is flagged388`"existing": true`, so pre-existing data survives. **Dry-run by default**; add `--apply389--allow-destructive` to actually delete (`--clear-workspace` also prunes `.maker-workspace/`).390**`--allow-destructive` is required for `teardown --apply`** — without it teardown refuses and391touches nothing.392393```bash394node "${PLUGIN_ROOT}/scripts/teardown-model-app.js" \395 --env <envUrl> --spec @<working-dir>/app-spec.json [--apply] [--allow-destructive] [--clear-workspace]396```397398**Safety flags (build + teardown).** The apply path is fail-closed against destructive operations:399400- **`--allow-destructive`** — authorize destructive operations. For `build --apply`: authorizes401 overwriting an existing app in unattended mode, and allows explicit-layout form-field removals or402 sitemap-target drops; also authorizes DETACHING a `pages-removed` page's nav subarea (the page403 record is left deployed — it is not deleted). For `teardown --apply`: **required** — all deletes404 are destructive by construction, so teardown without this flag halts before touching anything.405- **Pages-phase safety HALTs.** The build halts on identity or safety violations rather than406 proceeding with potentially wrong state. Surface the HALT reason and follow the recovery hint:407 - `pages-identity-conflict` — spec `pageId` and manifest disagree on a key, or a duplicate id408 spans two keys. Resolve the conflict manually (re-download or delete the manifest).409 - `pages-manifest-corrupt` — the manifest cannot be parsed (two keys map to the same id). Delete410 the manifest web resource and rebuild from scratch.411 - `pages-removed` — a live page was dropped from the spec. Re-add it, or pass `--allow-destructive`412 to detach it from the nav (the page record stays deployed; rebuild finalizes the sitemap).413 - `pages-shared-across-apps` — the page appears in another app's sitemap. Detach it in Maker.414 `--allow-destructive` does **not** bypass this halt.415 - `pages-shared-check-failed` / `pages-existence-failed` / `pages-sitemap-read-failed` — a416 prerequisite read failed; the build can't proceed safely. Retry on a transient error; check417 permissions on a persistent one.418- **`--non-interactive`** — suppress interactive prompts (for automation / CI). A non-interactive419 build that encounters an existing app **fails** instead of warning-and-proceeding, unless420 `--allow-destructive` is also set. Does **not** grant destructive authority on its own — only421 `--allow-destructive` does.422- **`POWER_PLATFORM_SKILLS_NONINTERACTIVE=1`** (or `true`) — env-var equivalent of423 `--non-interactive`. Same semantics: suppresses prompts only, never authorizes destructive ops.424 Set this in CI job environments to avoid interactive-prompt hangs.425- In **autopilot / eval mode** (`--non-interactive` + `--allow-destructive`), `preview-app.js`426 is written to disk as the design artifact before plan execution; interactive consent gates are427 bypassed and the build is fail-closed against any destructive op not explicitly authorized.428429### AI-first features430431The `ai` block in the App Spec controls four app-level features and per-table Copilot row432summaries. All features are **admin-gated**: they are enabled only where the environment433administrator has turned them on in Power Platform Admin Center (Environments → Settings →434Product → Features). The `ai-features` build phase preflights each setting via the SDK435(`RetrieveSetting`) and, for anything off, **skips it with a warning** — it never fails the436build and cannot flip admin or tenant switches itself.437438**Preflight (standalone):**439```bash440node "${PLUGIN_ROOT}/scripts/ai-preflight.js" --env <envUrl> [--app <uniqueName>]441```442Prints each feature's on/off status and the exact admin action required for anything that is off.443Never fails.444445**App-level features** (`ai.appFeatures`) — `formFill` (Copilot-assisted form fill), `nlSearch`446(natural-language grid/view search), `nlChart` (NL chart / AI data visualization), `m365` (M365447Copilot). All default to `true` except `m365`; set any to `false` to opt out.448449**Per-table row summaries** (`ai.summaries`):450- `default: "auto"` — the skill auto-selects good-candidate tables (skips lookup-only / config /451 junction tables and the Dynamics 365-owned `incident`, `lead`, `opportunity`).452- `default: "off"` — summaries disabled unless a table opts in explicitly.453- Per-table overrides in `summaries.tables`: set `enabled`, a tailored `instruction`, and454 `columns[]` (the fields the summary reads). A `{ "enabled": false }` entry opts a specific455 table out.456457**Prompt authoring guidelines** (for `instruction`): write for meaningful insights — not field/value458repetition; never include record GUIDs; pull in recent activity where relevant; use459audience-appropriate tone; aim for an explicit output shape (a short paragraph).460461**Teardown** removes the AI records created by the `ai-features` phase (summary config rows and462published AI models) in addition to the standard artifacts.463464---465466## Editing a deployed app (download → edit → rebuild)467468The **same App Spec drives edit** — there is no separate edit path. When the user wants to change an469existing app (add a field/view/page, edit a page's code, retitle/reorder nav, swap an icon), **pull the470deployed app fresh into a spec first**, then edit that spec and re-run Phase 2:471472```bash473node "${PLUGIN_ROOT}/scripts/download-model-app.js" --env <envUrl> --app <appId|uniqueName|displayName> --out <working-dir>474```475476`--app` resolves in identity order: app id (GUID) → `uniquename` → display name. Prefer the **unique477name** — it is immutable, while a display name can be renamed and is not unique. A display name that478matches more than one app is refused, listing the candidate unique names rather than guessing.479480This reconstructs the app into `<working-dir>/app-spec.json`. **Round-trip scope — be precise, it is481not everything:**482- **Round-trips:** the sitemap → `appShell` (all subareas + icons), **every** generative page (via483 `pac model genpage download`; names come from the sitemap's `GenPage` subarea titles, so484 Maker-added pages are included) into `pages[]` + their `.tsx`, the referenced entities (minimal —485 the build reuses existing tables), **classic dashboards** (id-passthrough tiles carrying the486 deployed view/chart ids), the icon web resources, and the solution.487- **Does NOT round-trip:** `forms[]`, `views[]`, `charts[]`, `commands[]` — they come back empty.488 All four **survive on the live app** (a rebuild preserves them by discovery), so a plain edit is489 safe; they just aren't editable through the downloaded spec. Change them in Maker or a fresh spec.490491Then:4924931. **Always pull fresh at the start of an edit session** — someone may have changed the app in Maker. The494 build reads an etag when it hydrates, so a write against an artifact changed since the pull throws a495 version conflict → **re-pull and retry**, never clobber.4962. Edit `app-spec.json` (and any page `.tsx`) for the requested change.4973. **If the edit ADDS a page** (or resets a page's `source` back to `intent` to regenerate it),498 **re-run Phase 1.5 before Phase 2** — a downloaded spec contains only `kind: "tsx"` pages, so an499 added `intent` page has no code and Phase 2's `deploy` profile rejects it. Phase 1.5 is safe to500 re-run: `write-page-plan.js` projects **all** pages (so the worker sees the real nav graph), a501 worker is dispatched only where the echo says `intent: true`, and `promote-intent-pages.js`502 leaves built pages untouched. Editing an *existing* page's `.tsx` by hand needs no regeneration.5034. Re-run the **build** (Phase 2). It's idempotent: it reuses the existing app/tables/views, **updates504 each page in place** (matched by name → `--page-id`, so no duplicates), and **preserves the505 existing `GenPage` subareas** (the download enumerated them into `pages[]`/`appShell`, so the506 full-replace sitemap write never drops them).5075. **Verify** (Phase 3) to confirm only the intended change landed.508509> **Prefer generative pages over classic dashboards** per the genpage-first policy. Classic dashboards510> do round-trip (id-passthrough tiles), but the views and charts their tiles point at do not, so they511> can only be edited in Maker.512513## What the builder does (in order)514515solution (idempotent) → data model — **discover** existing tables/columns/relationships via the SDK516(`findTables` / `findColumns` / `fetchEntityMetadata`) and create only what's missing (`createTable`517/ `createColumn` / `createRelationship`) → **sample data** (opt-in; relational/topological,518`$parent`→`@odata.bind` using the entity-set name) → **web resources** (opt-in;519`createWebResource` for form JS/HTML/CSS) → **views** → **charts** → **forms** (primary + columns520laid out, explicit `tabs` honored; sub-grids, quick-views, and form JS (`events[]`) applied as521canonical control cells / the `/bag/c` events region via the SDK's generic `addElement` surface)522→ **business rules** (`businessRules[]` — authored as the workflow object model through the bound523`CreateProcessWithWfomJson` member and activated; **skipped with a warning** on an environment that524does not declare that member) →525**business process flows** (`businessProcessFlows[]`; a category-4 process compiled from the526authored stages and activated — this goes through the SDK's generic artifact surface, a plain527`workflows` row, so it is NOT subject to the bound-member gate above) →528**command bar** (`commands[]`) → **classic dashboards** (opt-in)529→ **app module + sitemap** → **generative pages** (each page's `.tsx` was generated in Phase 1.5;530the build uploads each `pages[]` page via `pac model genpage upload`, no `--add-to-sitemap`; then531the SDK rewrites the sitemap once to add the `GenPage` subareas) → **AI features** (opt-in) →532**security** (one role per `personas[]` entry, sized from its jobs' declared access; injects an533app-module read privilege and associates the app to each role so it opens for that persona) →534publish (opt-in). When the app has generative-page subareas the app module is created first WITHOUT535them (they can't resolve until the pages upload), then the pages phase rewrites the sitemap. All536Dataverse access goes through the SDK, so the downloaded metadata lands in `.maker-workspace/`.537Independent ops (columns, views/charts/forms) run with bounded parallelism; publish is one538round-trip per entity + the app. Views/charts build **before** forms so a sub-grid can reference the539child view id. Each step emits `[n/total]`.540541## Notes & limits542543- **Headless, no browser.** The SDK (`cds-maker-sdk`, vendored at `scripts/vendor/`) generates544 designer-grade FormXML/FetchXML/sitemap by reusing the designer's own serializers, and writes via545 the Web API using an `az`-token HttpClient. No relay, no designer tab.546- **Dedicated unmanaged solution per app** (review / teardown). **`--publish` gates the final547 *bulk* publish** of the app's entity + app customizations (a `PublishXml` per entity + the app). It548 does **not** suppress the small **targeted** publishes that edit/finalize paths must run so the change549 takes effect — reconciling an existing form or view, wiring form events, placing quick-views,550 re-syncing an existing app's sitemap, and finalizing the sitemap after generative pages each publish551 that one artifact (an unpublished edit to a live artifact is invisible). A fresh build without552 `--publish` still leaves new tables/columns/relationships staged-but-unpublished in the solution.553- **Idempotent — but ADDITIVE, not yet full desired-state convergence.** Existing554 solution/tables/columns/relationships/views/charts/forms/commands/dashboards are detected and555 **reused**, so re-runs and existing-table envs work without collisions. **The caveat for 556557…(truncated)