跳到主要内容
知仓学习社ZHICANG

app-builder

Builds and edits a model-driven Power Apps app from a natural-language intent — tables, columns, relationships, adaptive forms with sub-grids, views…

不碰外部(只输出文字)无严重或高危命中microsoft/power-platform-skills

它会碰到什么

扫了多少1 个文本文件,47 KB
它会碰到什么不碰外部(只输出文字)
命中总数0 处
命中统计严重 0 · 高 0 · 中 0 · 低 0

这一栏是扫描器报的事实,不是结论。命中多不等于有毒(安全工具、规则库、示例脚本本来就会包含危险写法),命中少也不等于干净。它和你手上的凭据、文件、网络有什么关系,需要你自己看。

技能内容

> Plugin check: Run node "${PLUGIN_ROOT}/scripts/check-version.js" — if it outputs a message, show it to the user before proceeding.

app-builder — intent → model-driven app

Turn a natural-language intent into a deployed model-driven app. You author a reviewable App Spec

(JSON) with the user across confirmed turns, then a deterministic engine (cds-maker-sdk, vendored)

builds it — tables/columns/relationships, sample data, views, Choice-column charts, adaptive forms

with sub-grids, generative pages for overview/dashboard surfaces, and the app module + sitemap.

The same spec drives create and edit: download a deployed app back into a spec, change it, and

re-run the build (it's idempotent).

CRITICAL — run the interactive flow in THIS conversation (the main loop)

> **You MUST run the authoring questions and the build narration yourself, in the main

> conversation. Do NOT dispatch a subagent (Task) for the interactive steps.**

>

> A subagent is headless — AskUserQuestion and plan mode do not reach the user from inside one

> (its only output is its final message). The whole point of this skill is the multi-turn,

> propose-then-confirm experience, so every AskUserQuestion, EnterPlanMode, and live build

> status line must originate here, in the main loop.

CRITICAL — the user sees your chat message, NOT tool output

> **Shell/tool output — the result of running preview-app.js, a dry-run plan, or a lint —

> is COLLAPSED BY DEFAULT in the UI. The user does NOT see it unless they manually expand the

> tool panel.** Running the command is therefore NOT the same as showing the user. Every artifact

> the user must read, review, or approve — the whole-app preview wireframes, the

> dry-run build plan, and blocking lint findings — MUST be reproduced **verbatim in your

> chat reply**, inside a fenced ` code block. Never say "the preview looks right" and leave

> the content buried in a collapsed panel: paste it into your message. This is the #1 cause of

> "the wireframes aren't visible" — the preview ran, but its output stayed hidden.

Capabilities — the full toolbox (pick best-fit per requirement)

You are a complete model-driven app builder, not a single-surface tool. Everything below ships in

one App Spec and one build — choose what best serves the user's requirement to make a **useful,

prod-ready** app; don't under-build (a bare table list) or over-build (surfaces nobody asked for):

  • Data model — tables (give each custom table a meaningful Fluent-style SVG table icon by default; propose what the glyph will depict in words — never a Fluent token name — and record it as iconDescription before drawing the SVG — see [references/authoring-flow.md](../../references/authoring-flow.md) → Table icons), columns (all types), relationships (1:N / N:N + junctions), sample data
  • Record UI — forms (sub-grids, quick-create / quick-view), views (with enriched default columns), charts
  • Custom grid rendering (preview) — entities[].columns[].visualization: render a column as a

RadialDial, LineChart, HeatMap or StarRating in every grid and view that shows it,

instead of plain text. Reach for it when a column is a magnitude a user scans (a score, a

utilization %, a rating, a priority) rather than a value they read exactly — it makes a list

scannable at a glance for one line of spec. It is per-column, so set it once on the column, not

on each view. Preview: on an environment where it is not provisioned the build skips it and

everything else still deploys, so it is always safe to include.

  • Actions — modern command-bar buttons (incl. flyout / split menus), web resources (form JS / HTML / CSS).

Two rules when writing that JS, both learned from buttons that deployed perfectly and then did

nothing: a command handler is handed the record (function doThing(primaryControl)) — the build

supplies the parameter, so write that signature; and never hardcode Choice values like

100000003, because they are assigned per publisher. Resolve by label via getOptions()

(see references/app-spec-schema-advanced.md → webResources). Note also that command and web-resource

edits do not redeploy on rebuild — the phases reuse what exists, so changing a button or a

script means deleting it first.

  • Form logic without codebusinessRules[]: 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 processesbusinessProcessFlows[]: 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.

  • Surfacesgenerative pages (modern dashboards / overviews / analytics / landing — the default),

classic dashboards (opt-in), external URLs

  • App shell — the app module + sitemap, with per-subarea icons. Turn on the modern shell with

app.newLook: true unless the user asks for the classic one; it is opt-in and best-effort, so a

tenant without the setting still gets a working app. app.headerNavigationRefresh controls the

Wave 2 header/navigation refresh — a separate, independent setting whose platform default is

ON, so set it to false only when the user explicitly wants the classic header.

  • Security & access — one security role per persona, sized from that persona's jobs-to-be-done

(the entity access each job needs, unioned into the role), so the app opens for non-admins.

  • AI-first features (admin-gated) — form-fill assist, natural-language grid/view search, NL chart

/ AI data visualization, M365 Copilot (opt-in); per-table Copilot row summaries (Insight Cards)

with tailored prompts, auto-selected for good-candidate tables

Author the smallest spec that fully satisfies the ask, then let the user refine. The **Genpage-first

policy** below is the record-vs-dashboard rule; [references/app-spec-schema.md](../../references/app-spec-schema.md)

documents every field.

Genpage-first policy (surface classification)

Every app surface is one of two kinds — enumerate the app's surfaces and classify each one, don't

decide page-by-page in passing:

  • Record surfaces (create/read/update/list a table's rows) → a model-driven form + view.
  • Everything else — overview/landing, dashboard, KPIs, analytics, guided or wizard flow,

composite or comparison screen → a generative page (pages[]), not a classic dashboard.

Rules:

  • An app whose jobs include an overview, a queue, analytics or a guided flow but which proposes no

pages has missed a surface. Where the app genuinely is record-CRUD only, say so explicitly rather

than silently omitting pages.

  • A traditional dashboards[] is emitted only on explicit request (e.g. "use a classic dashboard").
  • A generative page is authored as a design intent (source: { kind: "intent" },

schemaVersion: 2) during Phase 1 — its .tsx is written in Phase 1.5 — Generate pages, after

plan approval and after the data pre-build creates the tables so pac model genpage generate-types

can emit RuntimeTypes.ts. See [references/authoring-flow.md](../../references/authoring-flow.md) → Pages.

  • The build's pages phase uploads each page via pac model genpage upload **without

--add-to-sitemap — the SDK is the single sitemap writer**, so a page's nav entry comes from a

page subarea in appShell (referenced by the page's key). See

[references/app-spec-schema.md](../../references/app-spec-schema.md) → pages[].

  • Every page in pages[] must be sitemap-placed — validation rejects any page absent from the

sitemap. A "detail" page that receives a caller-supplied id is a normal sitemap page; it reads its

input via pageInput?.data?.<field>. Navigation-only (headless) pages are not supported.

  • A page that declares pageInput MUST declare directEntry. Because every page is

sitemap-placed, a detail page is also reachable straight from the app navigation with no input

— a state a user reaches by clicking. Say what happens then: { "behavior": "selector" } shows a

picker and then the record, { "behavior": "emptyState" } explains and renders nothing broken.

Prefer selector when the table is browsable; it is the more useful landing. Every key in

pageInput.data must also be produced by some page's navigatesTo[].data, or the generated page

reads a key nothing ever sets.

  • Three-authority page identity (build + download + verify all follow this): (1) IDENTITY

the durable <app>_pagemanifest (key → pageId); a downloaded spec's own pages[].pageId

outranks it for that rebuild. (2) EXISTENCE — env-wide pac model genpage list (crash-safe;

decides create-vs-reuse). (3) MEMBERSHIP — the app's sitemap GenPageId set (placement,

download enumeration, verify coverage). All matching is by id — never by display name.

  • Multi-page navigation uses PAGEREF_<key> (the stable pages[].key) as the pageId

placeholder; the build resolves each to the real page GUID in a run-scoped staging copy (the

canonical .tsx is never mutated), then verifies every nav edge resolves to the deployed

page's GenPageId.

Workflow

Phase 0 — Working directory

  1. Derive a short kebab-case slug from $ARGUMENTS (e.g. "Project Tracker" → project-tracker).
  2. Create the directory (mkdir -p <slug> on bash/PowerShell; mkdir <slug> on cmd, which has no

-p and errors if it exists) and resolve its absolute path. It holds app-spec.json,

model-app-plan.md, and workflow-log.md.

Phase 1 — Author the App Spec (interactive, main loop)

Follow [references/authoring-flow.md](../../references/authoring-flow.md) step by step, running

every prompt yourself via AskUserQuestion. In short:

Unattended runs (Copilot autopilot / automation)

Resolve the interaction mode once before the first authoring question:

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. Prereqsnode --version, pac help (≥ 2.7.0).
  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).

  1. Detect existingpac model list-tables --search … (exact-match) and pac model list

to find tables/apps already present; build around them.

  1. Levelled authoringfirst read the App Spec format so you author to the exact

shape (do this once; don't go spelunking through scripts):

[references/app-spec-schema.md](../../references/app-spec-schema.md) and the worked sample

[samples/app-spec.support-desk.json](../../samples/app-spec.support-desk.json). That document

covers everything an app always has. **Additionally read

[references/app-spec-schema-advanced.md](../../references/app-spec-schema-advanced.md) once your

design calls for a conditional feature** — business rules, a business process flow, command-bar

buttons, web resources, global choices, classic dashboards, or roleGrants[]. Its pointer table

is in the main schema, and the toolbox above tells you when to reach for each; read the detail

only for the ones you are actually using, and never skip a capability just to avoid the read.

Phase 1 is

design-only: never emit page .tsx here. Each level is confirmed via AskUserQuestion before

the next begins, and app-spec.json is persisted after each — full prompts in the playbook.

  • Level (a0) — personas & jobs-to-be-done (personas[]): before proposing any tables, ask

who will use the app and what each needs to get done. Jobs drive everything that follows — a

table exists because a job needs its data, a surface because a job needs to act on it. Deriving

the data model first reliably misses surfaces. Privileges come later; capture the jobs now.

  • Level (a) — data model: entities/columns/relationships derived from those jobs; run the

early data-model lint (catches e.g. the relationship-vs-lookup collision before forms are

authored on top).

  • Descriptions are part of authoring, not a cleanup pass. Every table, column, view, chart,

form, dashboard and business rule takes an optional description, and you should **write one as

you create the artifact** — never as a backfill. A name says what a thing is called; a

description says what it is for, and it is the only intent an app carries that an agent can

read back later when it inspects an app it did not build. Describe the purpose, not the shape:

"Severity 1-5; drives the escalation rule and the SLA clock", not "The priority column".

(commands[] and Customer columns accept one but the SDK cannot write it — you'll get a

warning; personas[] does not take one at all. See the schema reference for why.)

  • 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.
  1. 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.

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

  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.

  1. 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.

  1. 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.

  1. 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.

  1. 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".

  1. 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,

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:

  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.

  1. Edit app-spec.json (and any page .tsx) for the requested change.
  2. 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.

  1. 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).

  1. 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) → viewschartsforms (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 + sitemapgenerative 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 EDITS: a

rebuild is additive — it creates what's missing but does not re-apply changes to an artifact

that already exists (a changed column type, a removed view column — reconcileView only adds

an edited form/command/dashboard), and never removes an artifact you dropped from the spec. **To

apply a structural edit, teardown --apply then rebuild fresh.** --verify catches this: it

checks content (a view's column set, relationship + command existence), so an unapplied edit

surfaces as a loud verify FAIL, not a false pass. Full in-place convergence is tracked in

docs/app-builder-capabilities.md.

  • 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, security-role grants).

  • 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); 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](../../docs/app-builder-capabilities.md) and

[references/app-spec-schema.md](../../references/app-spec-schema.md) (plus

[app-spec-schema-advanced.md](../../references/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.

想直接用这个技能?

本站把开放许可(MIT / Apache 等)的技能按仓库打包整理到网盘,点一下转存到你自己的网盘,不用一个个从 GitHub 拉。许可未声明的技能只给原始仓库链接,不打包。

它属于哪个仓库

星标★ 878
本站分层T2
该仓技能数113
原文件路径plugins/model-apps/skills/app-builder/SKILL.md

同一个仓库里的其他技能

看这个仓库的全部 113 个技能