Power Apps Generative Pages Builder skill

Creates, updates, and deploys Power Apps generative pages for model-driven apps using React v17, TypeScript, and Fluent UI V9.

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

Use now

Files of Power Apps Generative Pages Builder

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

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

Power Apps Generative Pages Builder

Triggers: genpage, generative page, create genpage, genux page, build genux, power apps page, model page Keywords: power apps, generative pages, genux, model-driven, dataverse, react, fluent ui, pac cli Aliases: /genpage, /gen-page, /genux

Overview

This skill orchestrates specialist agents across the create and edit flows:

Create flow:

  1. genpage-planner — validates prerequisites, gathers requirements, detects what entities and apps exist, and returns a proposed plan for the orchestrator to present. Writes genpage-plan.md once the orchestrator re-invokes it with the approval outcome. May pause and return connector_discovery_required (see 2).
  2. genpage-connector-builder — top-level orchestrator dispatch for connector feature-gating and discovery; writes connector-bindings.md + connectors.json. Runs after the planner has resolved create-vs-edit and the target environment (discovery is mutating), then the planner is re-invoked with its contract. genpage-customapi-builder — the same top-level dispatch for server-side Custom API (Action/Function) needs; writes custom-api-bindings.md + actions.json.
  3. genpage-entity-builder — creates Dataverse entities (tables, columns, relationships, choices, sample data) via the plugin's Node.js Web API scripts
  4. genpage-page-builder — generates one complete .tsx file per page; multiple builders run in parallel for multi-page requests

Edit flow:

  1. genpage-connector-builder — top-level orchestrator dispatch when an edit adds, replaces, discovers, removes, or clears connector bindings; preserves unchanged bindings when the edit does not touch them. genpage-customapi-builder — the same top-level dispatch when an edit adds, replaces, discovers, removes, or clears Custom API bindings.
  2. genpage-edit-planner — reads the downloaded page artifacts, gathers change requirements, presents an edit plan, writes genpage-edit-plan.md

You (the skill) coordinate the agents and own connector and Custom API dispatch, app creation, RuntimeTypes generation, deployment, browser verification, and the inline application of planned edits.

References

Development Standards

  • React 17 + TypeScript — all generated code
  • Fluent UI V9 — @fluentui/react-components exclusively (DatePicker from @fluentui/react-datepicker-compat, TimePicker from @fluentui/react-timepicker-compat)
  • Single file architecture — all components, utilities, styles in one .tsx file
  • No external libraries — only React, Fluent UI V9, approved Fluent icons, D3.js for charts
  • Type-safe DataAPI — use RuntimeTypes when Dataverse entities are involved
  • Responsive design — flexbox, relative units, never 100vh/100vw
  • Accessibility — WCAG AA, ARIA labels, keyboard navigation, semantic HTML
  • Complete code — no placeholders, TODOs, or ellipses in final output

Instructions

Follow these phases in order for every /genpage invocation.

Every <…> you fill into a command goes in single quotes ('<working-dir>/prompt.txt', '<App Name>'). PowerShell and bash expand nothing inside them — not $name, not $(…), not a backtick — and a space does not split the value. Escape a single quote inside the value the shell's way: in PowerShell double it, the typographic ‘ ’ included ('Bob''s app'); in bash close, escape and reopen ('Bob'\''s app'). Never use double quotes for a value ("Revenue $100" arrived as Revenue ) and never leave one bare (D:\Work Projects\… became two arguments). A value holding a double quote " is not put on a command line at all — Windows PowerShell 5.1 drops it and can split the argument there — so ask for an app or solution name without one. Free text (a prompt, an agent message, a page's display name) never goes into a shell command at all, however it is quoted: even a single-quoted here-string ends at a line that begins with '@, and the rest of that line runs. Write it with your file-writing tool and pass the path (Phase 6).

Phase 0: Create Working Directory

Derive a short folder name from the user's requirements:

  1. Extract the page name or a 2-4 word summary from $ARGUMENTS
  2. Convert to kebab-case (e.g., "Candidate Tracker" → candidate-tracker)
  3. Create the folder: mkdir -p '<folder-name>' (PowerShell or bash, the shells every command in this skill is written for)
  4. Resolve its absolute path — this is the working directory for all subsequent phases
Phase 0.5: Initialize Local-Dev Manifest

Write package.json and genpage.d.ts into the working directory so the developer can npm install and get IntelliSense, type-checking, and "go to definition" in their editor. Versions come from references/supported-dependencies.md (single source of truth: scripts/lib/supported-dependencies.js).

node "${PLUGIN_ROOT}/scripts/generate-page-manifest.js" '<working-dir>' '<kebab-slug>'
  • <kebab-slug> is the same slug used for the working directory.
  • Add --features charts,datepicker,timepicker (comma-separated) only when the requirements clearly call for them; otherwise omit and keep the manifest lean.
  • The script keeps an existing package.json (no --force) only when it already lists every package the requested features need. A package present at a version the user changed is kept and listed as versionDrift in the JSON summary — mention it, since local type-checking then differs from the versions pages are written for. Pass --force to overwrite (used in regeneration flows when versions drift).
  • Output is a JSON summary on stdout. Continue only on exit 0.
    • Exit 2 — halt and show the user the error line. When it names packages a requested feature needs that the existing package.json lacks, ask whether they will merge them into it or want a rerun with --force (which replaces the file), and rerun until it exits 0 before Phase 1 — continuing would leave a --features charts run on a manifest without d3, the stale state this check exists to stop. Exit 2 also reports a working directory that is a link or not a directory: never write through it.
    • Any other non-zero exit is a usage error in the command above: fix it and rerun.
Phase 1: Plan

⚠️ CRITICAL — the interactive steps run HERE, in the main conversation loop. You MUST NOT dispatch them to a Task subagent.

A Task subagent is headless: AskUserQuestion, EnterPlanMode and ExitPlanMode never reach the user from inside one. A flow that specifies them there cannot complete — the question is never answered and the approval never given. This is the same rule /app-builder follows, and the reason its subagents are headless workers only.

genpage-planner is therefore a headless discovery agent. It runs the read-only work and returns what it found; you ask the questions and present the plan.

Run in the main loop (never delegated):

  1. Prerequisite validation (node --version, pac help version > 2.10.0)
  2. Auth verification (pac auth list, environment selection)
  3. The structured "Create new / Edit existing" question (AskUserQuestion)
  4. Language detection (pac model list-languages) — only on new-page path
  5. Entity existence detection (pac model list-tables --search)
  6. App detection (pac model list) with proper selection prompts
  7. Plan-mode presentation and approval (EnterPlanMode / ExitPlanMode)
  8. Telling genpage-planner the approval outcome — it writes genpage-plan.md itself — and confirming the file exists before Phase 2

Steps 1, 2, 4, 5, 6 are read-only discovery and may be delegated to genpage-planner; steps 3, 7 and 8 never can. Delegating the discovery is an optimisation, not a requirement — running it inline is equally correct, but genpage-plan.md is still written by the planner either way (see step 6 of the Steps list): its section headings are a machine-readable contract every downstream phase parses by name, so it has exactly one author. If you ran the discovery inline, dispatch the planner once with what you found, so it has the context to produce the plan without repeating your reads.

Never skip the prereq/auth steps, even when $ARGUMENTS already states the intent. A stated intent lets you skip question 3; it does not establish that the CLI is present, authenticated, or pointed at the right environment.

Whoever runs a step records it in workflow-log.md in the documented format — AskUserQuestion: <question> → <answer>, EnterPlanMode called followed by the response. The log is the contract the eval harness reads, and it does not care which loop made the call.

Unattended runs (Copilot autopilot / Claude auto-accept)

Copilot CLI autopilot and Claude Code auto-accept drive this skill with no user watching. Every gate below is written as "ask the user", and in those modes there is nobody to answer: the run either stalls on a question no one sees or, worse, records an answer nobody gave. Resolve the mode once, at the start of Phase 1, and carry it through every gate:

node "${PLUGIN_ROOT}/scripts/resolve-interaction-mode.js"

One JSON line, always exit 0 — "there is no user" is a fact about the run, not a failure of it:

{ "ok": true, "interactive": false, "reason": "POWER_PLATFORM_SKILLS_NONINTERACTIVE is set" }

A run is unattended when --non-interactive is passed or POWER_PLATFORM_SKILLS_NONINTERACTIVE is 1/true — the same switch /app-builder already uses, so one setting covers both skills.

When interactive is false, do not call AskUserQuestion, EnterPlanMode or ExitPlanMode at all. Take the documented default and record it in workflow-log.md as Unattended default: <question> → <answer> (<reason>), so the log still shows what decided the run:

Gate Attended Unattended
Recording the resolved mode Nothing to record Write the mode itself as Unattended default: interaction mode → unattended (<reason>) before the first gate. Record it with this marker, not as free prose such as Interaction mode: unattended — the evaluator keys on the marker, and prose forms are indistinguishable from an attended log that merely mentions the word (Mode: unattended = false, not unattended).
Create new / edit existing (step 2) AskUserQuestion Whatever $ARGUMENTS states. With nothing stated, create new — the only additive choice.
An agent returns needs_input (step 4) Ask, then re-invoke Re-invoke with the option the agent marked "default": true. If it marked none, halt.
Plan approval (step 5) EnterPlanMode / ExitPlanMode Treat the plan as approved and continue to step 6, which still writes genpage-plan.md through the planner. The plan is recorded, just not presented. Log this gate as Unattended default: plan approval → approved (<reason>) — the evaluator looks for the plan/approval wording and approved on that one line, so a paraphrase such as → auto-approve is read as a missing approval record.
Browser verification (Phase 7) Offer it Skip it.

Suppressing a prompt never authorizes destructive work. Editing an existing page overwrites source nobody reviewed, so on the edit path an unattended run requires the page to be named explicitly in $ARGUMENTS. Do not infer the target from a search result and do not fall back to "the only page that matched". If the target is ambiguous, halt and say so — an unattended run that guesses which page to overwrite is the one failure this table exists to prevent.

Halting is a normal outcome here, not an error to route around: report what was missing and stop, so the run can be re-driven with the decision supplied.

Steps
  1. Run the prerequisite, auth and discovery steps (inline, or via genpage-planner as a headless worker). Connector discovery has not run yet, so the contract is the literal No connector bindings., with discovery available on request (see 1a). Custom API discovery is likewise orchestrator- owned and has not run either, so its contract starts as the literal No custom API bindings., with discovery available on request (see 1b).

  2. Ask question 3 (create new / edit existing) with AskUserQuestion, unless $ARGUMENTS already settles it. On edit, jump to the Edit Flow section.

  3. If discovery reports { "action": "connector_discovery_required" }, invoke genpage-connector-builder with the intent — Mode: create for a new page, Mode: edit for an edit — using the resolved environment URL, then re-run discovery with the builder's ## Connector Bindings contract and connectors.json status. If it instead reports { "action": "custom_api_discovery_required" }, invoke genpage-customapi-builder the same way (same mode, resolved environment URL, plus the returned pageTables), then re-run the planner with the builder's ## Custom API Bindings contract and actions.json status (see 1b). If genpage-connector-builder or genpage-customapi-builder instead reports that its declared file or process-execution tools are unavailable, do not retry the same worker. Record the worker failure, read that worker's agent file, and run the discovery-builder workflow inline in this orchestrator using the same resolved mode, environment, intent, and safety gates. This inline fallback is recovery from task-runtime tool exposure only; it does not bypass connector/custom-API feature gates or user decisions.

  4. If any agent returns { "action": "needs_input", … }, ask its questions here with AskUserQuestion, record them in workflow-log.md, and re-invoke that agent with the answers. Agents never prompt; they request.

  5. Present the plan with EnterPlanMode and get approval via ExitPlanMode. On a revision request, re-invoke the planner with the requested revisions and present the revised plan again.

  6. On approval, re-invoke genpage-planner with the approval outcome plus the plan body it returned and everything it already discovered. The planner writes genpage-plan.md in its own final step, and it only reaches that step when it is told the plan was approved — a Task subagent is headless, so it cannot see the ExitPlanMode result any other way. A re-invocation is a fresh run with no memory of the last one: carry the state forward or it will re-ask questions the user has already answered, or re-derive a plan that is not the one they approved. Same rule as Phase 2b. Do not write the file yourself — its section headings are a machine-readable contract that every downstream phase parses by name.

    Before that approval writeback dispatch, quarantine any previous authoritative plan so a stale file cannot satisfy the post-dispatch existence check:

    node "${PLUGIN_ROOT}/scripts/genpage-plan-provenance.js" prepare --plan '<working-dir>/genpage-plan.md'
    

    Continue only on "ok":true. It refuses a plan path, approval sidecar (.approved-genpage-plan.md) or .genpage-provenance folder that is a link or junction (a dangling one included) or the wrong kind of entry, since the approved plan would be written through it: on "ok":false, halt and tell the user to remove what the error names.

    Also save the plan body the planner returned for approval — the one presented with EnterPlanMode, or approved by default when unattended — to a sidecar such as <working-dir>/.approved-genpage-plan.md (not genpage-plan.md), exactly as returned. The planner writes a different document from it (the schema file, with suffix-only names), so the verifier compares what both name as targets: the pages to build.

    If genpage-planner reports that the file tools needed to write the approved genpage-plan.md are unavailable, do not retry it and do not write the plan inline. Halt with the approved plan body and failure recorded. Planner authorship is the provenance gate for every downstream phase.

  7. Confirm <working-dir>/genpage-plan.md exists before starting Phase 2. Reaching Phase 2 without it means building from a plan nobody approved, and Phase 2 reads that file as its first action.

    If it is missing, do not proceed and do not write it yourself. Re-invoke the planner once more with the approval outcome and the plan body after running the prepare command above again. If it is still missing, stop and tell the user what was approved and what failed to be written — a hand-written substitute is a plan with no provenance, and every later phase will treat it as approved.

    If it exists, verify its provenance before Phase 2:

    node "${PLUGIN_ROOT}/scripts/genpage-plan-provenance.js" verify --plan '<working-dir>/genpage-plan.md' --approved '@<working-dir>/.approved-genpage-plan.md'
    

    Continue only when the JSON result has "ok":true, and record its writtenHash in workflow-log.md. If the written plan targets other pages than the approved plan named — an extra, missing or renamed page file — halt: downstream phases would build pages the user did not approve.

1a. Connector discovery is orchestrator-owned and never speculative

genpage-connector-builder is dispatched only by this top-level orchestrator, not by genpage-planner. This keeps connector discovery in one agent while avoiding nested Task calls from the planner.

Never run discovery before the planner returns — not even when $ARGUMENTS obviously mentions SharePoint, Teams, Office 365 or a custom REST source. Discovery is a mutating operation: it can create a connection reference. The planner is what resolves (a) create vs. edit and (b) which environment, and it may resolve either differently from the active pac auth profile. A connection reference created in the wrong environment, or in create mode for what turns out to be an edit, cannot be undone by discarding the local outputs.

So the sequence is always: plan first, then discover, then re-plan.

  • Every first planner invocation gets the literal contract No connector bindings. and is told discovery has not run.
  • When the planner determines connector-backed data is needed — from $ARGUMENTS or from its own user clarification — it returns { "action": "connector_discovery_required", "intent": "..." } together with the resolved action and environment URL.
  • Only then dispatch genpage-connector-builder with that mode, that environment URL, the working directory and ${PLUGIN_ROOT}. Read <working-dir>/connector-bindings.md and verify <working-dir>/connectors.json is a bare JSON array, then re-run the planner with the refreshed contract.

The builder remains the single owner of connector discovery: it writes No connector bindings. + [] when the page needs no connector, and performs all connection discovery only when one is required.

1b. Custom API discovery is orchestrator-owned too

genpage-customapi-builder is likewise dispatched only by this top-level orchestrator, not by genpage-planner — the planner has no Task tool, and the builder may need to ask the user which Custom API to bind, which only the main loop can do. Custom API discovery is read-only (a Web API query over the Custom API tables), so unlike connector discovery it carries no "wrong environment / wrong mode cannot be undone" hazard. It still runs after the planner so it targets the environment the planner resolved and binds to the page tables it detected.

  • Every first planner invocation gets the literal contract No custom API bindings. and is told discovery has not run.
  • When the planner determines a server-side Custom API (Action/Function) is needed — from $ARGUMENTS or from its own user clarification — it returns { "action": "custom_api_discovery_required", "intent": "...", "resolvedAction": "create", "envUrl": "...", "pageTables": "..." }.
  • Only then dispatch genpage-customapi-builder with that mode, environment URL, page tables, the working directory and ${PLUGIN_ROOT}. Read <working-dir>/custom-api-bindings.md and verify <working-dir>/actions.json is a bare JSON array, then re-run the planner with the refreshed contract.

The builder remains the single owner of the custom-api feature gate: it probes first, writes No custom API bindings. + [] when the gate is off or the page needs no Custom API, and performs discovery only when one is required.

Invocation prompt

Pass a prompt that includes:

  • The user's requirements: $ARGUMENTS
  • The working directory (absolute path from Phase 0)
  • The plugin root path: ${PLUGIN_ROOT}
  • The connector contract: the full body of <working-dir>/connector-bindings.md, or the literal No connector bindings. when discovery was not needed
  • The connector upload file status: <working-dir>/connectors.json exists and is a bare JSON array, or no connectors.json; omit --connectors
  • The Custom API contract: the full body of <working-dir>/custom-api-bindings.md, or the literal No custom API bindings. when discovery was not needed
  • The Custom API upload file status: <working-dir>/actions.json exists and is a bare JSON array, or no actions.json; omit --actions

Example:

You are the genpage-planner agent. Plan generative page(s) for the following requirements:

[paste $ARGUMENTS here verbatim, or "no arguments provided — gather from user"]

Working directory: [absolute path from Phase 0] Plugin root: ${PLUGIN_ROOT}

Connector discovery is orchestrator-owned. Do not invoke genpage-connector-builder from inside the planner. The ----- BEGIN/END CONNECTOR BINDINGS ----- lines below are delimiters for this prompt only: they mark where the contract starts and ends. Do not copy them into genpage-plan.md. The ## Connector Bindings section of the plan is exactly the text between them:

----- BEGIN CONNECTOR BINDINGS ----- [paste connector-bindings.md body, or No connector bindings.] ----- END CONNECTOR BINDINGS -----

Connector upload file (orchestration metadata; not part of the ## Connector Bindings section): [absolute path to connectors.json, or none — omit --connectors]

If your clarification questions reveal connector-backed data that is not covered by the connector contract above, stop and return { "action": "connector_discovery_required", "intent": "<connector need>", "resolvedAction": "create" | "edit", "envUrl": "<the environment you resolved>" } instead of trying to discover connectors yourself. resolvedAction and envUrl are required — discovery is dispatched against exactly those.

Custom API discovery is orchestrator-owned too. Do not invoke genpage-customapi-builder from inside the planner. The ----- BEGIN/END CUSTOM API BINDINGS ----- lines below are delimiters for this prompt only: they mark where the contract starts and ends. Do not copy them into genpage-plan.md. The ## Custom API Bindings section of the plan is exactly the text between them:

----- BEGIN CUSTOM API BINDINGS ----- [paste custom-api-bindings.md body, or No custom API bindings.] ----- END CUSTOM API BINDINGS -----

Custom API upload file (orchestration metadata; not part of the ## Custom API Bindings section): [absolute path to actions.json, or none — omit --actions]

If your clarification questions reveal a server-side Custom API (Action/Function) not covered by the Custom API contract above, stop and return { "action": "custom_api_discovery_required", "intent": "<operation>", "resolvedAction": "create" | "edit", "envUrl": "<the environment you resolved>", "pageTables": "<page tables or none>" } instead of trying to discover it yourself.

Follow the instructions in your agent file. Validate prereqs and confirm auth. The create/edit decision and the resolved environment are supplied to you by the orchestrator (it asks; you are headless) — use them rather than prompting. If you need any further decision, return { "action": "needs_input", … }. Write genpage-plan.md to the working directory once the orchestrator reports the plan approved. Return the page list, entity status, app selection, and any { "action": "edit" } signal when complete.

Phase 2: Create Entities (Conditional)

Read genpage-plan.md from the working directory. Check the Entity Creation Required section.

If the section literally says "No entity creation required — all entities already exist": Skip to Phase 3.

If entities need creating:

2a. Pre-flight: az + pac + Dataverse

Entity creation runs through the plugin's Node.js Web API scripts using az for auth, and the az and pac identities should normally match. Run the consolidated pre-flight:

node "${PLUGIN_ROOT}/scripts/check-auth.js" --require-pac

Genpage deploys pages via pac model genpage, so pass --require-pac to keep a missing pac login a hard blocker (the app-builder skill omits the flag — its build path only needs the az token). It returns a single JSON object:

{
  "ok": true | false,
  "blocker": null | "usage" | "az_missing" | "az_not_logged_in" | "az_timeout" | "pac_not_logged_in"
                 | "pac_timeout" | "no_env_url" | "whoami_403" | "whoami_401" | "whoami_error",
  "message": "human-readable next step",
  "warnings": ["..."],
  "azUser": "...", "pacUser": "...", "envUrl": "...",
  "identitiesMatch": true | false,
  "whoAmI": { "ok": true, "userId": "...", "organizationId": "..." }
}
  • ok: true and identitiesMatch: true → proceed to 2b.
  • ok: true and identitiesMatch: false → proceed to 2b but surface the message to the user as an inline warning ("az is X, pac is Y — WhoAmI works for now, but if entity creation later returns 403, run the suggested az login --username to align them").
  • ok: false → show the message field to the user verbatim and stop the workflow. The script already includes a fix-it command for every blocker (run az login, etc.).
  • blocker: "usage" is the one exception: the check-auth.js command line itself was wrong (a mistyped flag or a missing value). Fix the invocation and run it again instead of stopping.
  • blocker: "az_timeout" means the Azure CLI was too slow to answer, not that it is missing or signed out. Retry once; if it recurs on a busy machine, set POWER_PLATFORM_SKILLS_AZ_TIMEOUT_MS (milliseconds, default 60000) for the Azure CLI budget.
  • blocker: "pac_timeout" means pac org who did not answer within its fixed 60 s, so the PAC login is unknown, not missing. Retry once. The Azure CLI setting above does not change this budget.

Capture envUrl from the result — Phase 2b passes it to the entity-builder.

2b. Invoke entity-builder

Invoke the genpage-entity-builder agent via the Task tool. Pass in the prompt:

  • Path to genpage-plan.md
  • Working directory (absolute path)
  • Plugin root: ${PLUGIN_ROOT}
  • Dataverse env URL (from pac org who)

The entity-builder reads Solution and Publisher Prefix directly from the plan's ## Environment — no need to re-thread them here.

Wait for completion. The builder writes a transactional log at <working-dir>/genpage-entity-creation-log.md for recovery on failure.

The builder is headless and will ask for the sample-data decision by returning { "action": "needs_input", … } (it has no way to prompt). Handle it here, in this loop, exactly as Phase 1 step 4 does:

  • Ask each question with AskUserQuestion.
  • Record every exchange in workflow-log.md as AskUserQuestion: <question> → <answer>.
  • Re-invoke the builder with the answers plus everything it already returned, so it can carry out the step that depended on them (sample data is created by a second pass of its own CLI, not by anything here). A re-invocation restarts the agent at its first step, which is safe — provision-entities.js is idempotent and re-reports the existing tables rather than recreating them — but say which decision has now been answered so it goes on to the sample-data step instead of asking again.

Repeat until it returns a completion rather than a request. Proceeding to Phase 3 on a needs_input return silently drops the decision the user was asked to make.

Phase 3: App Creation/Selection

Read genpage-plan.md for the app decision and the Solution line in ## Environment.

If "create new":

pac model create --name '<App Name>' --solution '<Solution unique name>' --publish

--solution is mandatory. pac model create errors out with "The given solution name is not valid: ()" if you omit it — its claimed "active solution" fallback does not work in practice.

--publish is mandatory. Without it the new appmodule stays in draft and the genux runtime URL errors with "app not published".

  • Use the plan's Solution value verbatim. The planner always writes one (default fallback is literally Default).
  • If the plan is somehow missing Solution, pass --solution Default — every Dataverse env has a built-in "Default Solution" by that unique name.

Store the new app-id for Phase 6.

If existing app-id: Use it directly. pac model create is not called, so the Solution line is informational only for this phase.

Phase 4: Generate RuntimeTypes (Conditional)

If any page uses Dataverse entities, generate the TypeScript schema:

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

Windows + Bash: Always use forward slashes in file paths (e.g., D:/temp/RuntimeTypes.ts).

After generating, read the RuntimeTypes.ts file to verify it generated correctly.

For mock data pages only: Skip this phase.

Phase 4.5: Connector Bindings (Conditional)

Read the plan's ## Connector Bindings section and treat it as bindings only when it contains an actual binding table (a | Logical Name | … header with at least one data row). If the section is No connector bindings., empty, missing, or malformed, the page has no connectors: skip this phase entirely — do not create or pass connectors.json, and do not add --connectors on upload.

Carry this decision into code generation. The outcome is Connectors: <n> binding(s) or Connectors: none for the rest of the run, and Phase 5 must pass it verbatim in every page-builder dispatch — otherwise the generated page could call a connector this run never binds, and the page fails at runtime instead of simply omitting the feature. The dispatch value is the binding count, not a flag state: an empty binding table produces none, because the page-builder only ever needs to know how many bindings it may call.

When there are real bindings, the genpage-connector-builder agent already wrote <working-dir>/connectors.json during planning — verify it exists and matches the plan table. If it is missing, derive it from the plan table as a bare JSON array (never the { "connectorBindings": [...] } object wrapper — that is the deployed page config.json shape that pac writes):

[
  {
    "logicalName": "new_uxtest_sharepoint",
    "connectorId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
    "dataset": "https://host.sharepoint.com/sites/x",
    "tables": ["5709dd6f-c73e-4079-ad23-2334e45e0e13"],
    "tableDisplayNames": ["Pet"]
  },
  {
    "logicalName": "new_uxtest_msnweather",
    "connectorId": "/providers/Microsoft.PowerApps/apis/shared_msnweather",
    "dataset": "",
    "operations": ["CurrentWeather"]
  }
]

Do not write connection IDs into connectors.json — the importing maker/admin fills env-specific ConnectionId values through solution deployment settings.

Phase 4.6: Custom API Bindings (Conditional)

Re-probe the feature gate here — do not rely on the plan content alone. A plan authored while the flag was ON must not deploy Custom API bindings after it is turned OFF:

node "${PLUGIN_ROOT}/scripts/lib/feature-flags.js" custom-api

If it prints disabled: Custom API support is OFF. Do not create or pass actions.json, and never add --actions on upload. The plan's ## Custom API Bindings section still decides whether the run may go on: only a body of exactly No custom API bindings. continues, with no Custom APIs. For an actual binding table (a | Name | Kind | … header with at least one data row), halt before page generation. That plan was made while the flag was on, and page generation takes the table as permission to emit executeAction / executeFunction calls — deploying them with no bindings leaves pages calling Custom APIs that are not bound. Tell the user to turn the flag back on, or re-run planning (the Custom API builder writes no bindings while the flag is off). A missing, empty, or malformed section halts too, exactly as in the enabled branch. (Backstop: list-custom-apis.js also fails closed with exit 3 if invoked while OFF.)

If it prints enabled: read the plan's ## Custom API Bindings section. The section is mandatory: continue with no actions only when its body is exactly No custom API bindings.. Treat it as bindings only when it contains an actual binding table (a | Name | Kind | … header with at least one data row). If the section is missing, empty, or malformed, halt before page generation; do not interpret a planner/schema failure as "no Custom APIs".

When there are real bindings, the genpage-customapi-builder agent already wrote <working-dir>/actions.json during planning — verify it exists and matches the plan table. If it is missing, derive it as a bare JSON array of { name, isFunction, boundEntityLogicalName?, displayName, parameterKinds } entries (Action row isFunction:false, Function true; boundEntityLogicalName only for an entity-bound, non- (Global), row) — see ${PLUGIN_ROOT}/references/custom-api.md. Never the { "actionBindings": [...] } object wrapper (the deployed config.json shape pac writes).

Phase 4.7: Page Telemetry (Conditional)

Re-probe the feature gate here — do not rely on the plan content alone.

node "${PLUGIN_ROOT}/scripts/lib/feature-flags.js" custom-telemetry

This phase has no bindings and no artifacts; it only decides whether page-builder is permitted to instrument. If it prints disabled, generated pages contain no telemetry calls at all — identical to before the feature existed.

Carry this decision into code generation. The probe result is Telemetry: disabled / Telemetry: enabled for the rest of the run, and Phase 5 must pass it verbatim in every page-builder dispatch.

enabled is permission, not instruction. Even when it is on, page-builder emits telemetry only when the maker asked to measure or track something in their own words; the default output is still a page with zero telemetry. See ${PLUGIN_ROOT}/references/page-telemetry.md.

Phase 5: Build Pages (Parallel)

Read genpage-plan.md and extract the pages table.

5a. Validate the plan before dispatch

Before invoking any builders, verify:

  • At least one page exists in the ## Pages table

  • Every page has a ### [Page Name] subsection in ## Per-Page Specifications

  • All filenames in the ## Pages table are safe and unique. Run the deterministic gate — it applies the same rule as the plan validator and /app-builder:

    node "${PLUGIN_ROOT}/scripts/check-page-files.js" --plan '<working-dir>/genpage-plan.md'
    

    Continue only on "ok":true. It reads the plan's one ## Pages table: a plan with a second Pages table that has a File column is refused, not read by its first — one quoted in the requirements, fenced, quoted or not, counts. It refuses absolute paths (drive-qualified ones included), .. traversal, backslash separator aliases, a character a shell would expand or split on (a space, $, a backtick, ; — page file names use letters, digits, ., - and _), a name Windows cannot store (a device name such as CON.tsx, a reserved character such as :, or a trailing dot or space), a name that is not a .tsx page file (such as package.json or RuntimeTypes.ts), a page path that is itself a link, junction or hard link or is not a regular file, a parent that resolves through a link or junction to outside the working directory or cannot be resolved at all (a dangling link, a folder it cannot read), and case-insensitive collisions — Page.tsx plus page.tsx in the plan, two names that reach one file through a link, or a name whose file or folder is already on disk under another spelling. On any problem, halt and re-plan instead of rewriting filenames here: a renamed file is a page the user did not approve, and the provenance check compares page files. Duplicate filenames cause silent last-writer-wins data loss under parallel execution.

See ${PLUGIN_ROOT}/references/plan-schema.md for the full contract.

5b. Single-page fast path (skip Task dispatch when N=1)

If the plan's Pages table contains exactly one row, do NOT dispatch a Task subagent. Inline the page-builder workflow directly in the orchestrator:

  1. Read ${PLUGIN_ROOT}/references/rules.md

  2. Read the sample listed in the plan's ## Relevant Samples

  3. Only when the plan's ## Connector Bindings section contains an actual binding table (a | Logical Name | … header with at least one data row), also read ${PLUGIN_ROOT}/references/connectors.md. Treat a No connector bindings. sentinel or an empty/missing/malformed section as having no connectors (same contract as Phase 4.5 and genpage-page-builder). 3b. Only when the Phase 4.6 probe printed enabled and the plan's ## Custom API Bindings section contains an actual binding table (a | Name | Kind | … header with at least one data row), also read ${PLUGIN_ROOT}/references/custom-api.md. Treat a No custom API bindings. sentinel as no Custom APIs. If the section is missing, empty, or malformed, halt before inline generation (same contract as Phase 4.6); do not downgrade a planner/schema failure to "no Custom APIs." (A disabled probe with a binding table has already halted in Phase 4.6.)

  4. If the plan's Per-Page Specification has Needs caching: true, also read ${PLUGIN_ROOT}/references/data-caching.md

  5. If the plan's ## Environment indicates non-English languages, also read ${PLUGIN_ROOT}/references/localization.md 5b. Only when the Phase 4.7 probe printed enabled and the maker's own request asks to measure, track, monitor, or diagnose something, also read ${PLUGIN_ROOT}/references/page-telemetry.md. In every other case the page contains no telemetry calls — do not read it.

  6. Read genpage-plan.md (already in working directory) and RuntimeTypes.ts if Data mode is dataverse

  7. Stamp the target, so the gate below can tell the page you write from one an earlier attempt left there. Here and below, <filename> is the page's File value from the plan without its .tsx extension (candidate-tracker.tsx gives candidate-tracker):

    node "${PLUGIN_ROOT}/scripts/genpage-worker-output.js" --stamp --file '<working-dir>/<filename>.tsx'
    

    Then write the .tsx file to <working-dir>/<filename>.tsx following all rules

  8. After writing, Grep every named import from @fluentui/react-icons against ${PLUGIN_ROOT}/references/verified-icons.txt (one Grep per name). Rewrite any unverified names with the closest verified alternative; do not load the full icon list into context

  9. Grep the generated file with ['"]?borderWidth['"]?\s*:. Griffel rejects that shorthand only at runtime; the regex catches unquoted, quoted, and whitespace-separated property syntax. Replace every match with the four explicit border-side widths before deployment.

  10. Run the completeness gate on the page you wrote — the same one 5c runs on a worker's page, and this is also the page a 5c fallback produces:

    node "${PLUGIN_ROOT}/scripts/genpage-worker-output.js" --file '<working-dir>/<filename>.tsx'
    

    On "ok":false, rewrite the page once from the same plan inputs and run the gate again; if it still fails, halt with the reported problems rather than deploy an incomplete page.

  11. Proceed to Phase 6

This saves ~5-15s of Task overhead and ~3K tokens that would otherwise be duplicated in a subagent context.

5c. Multi-page: invoke page-builders in parallel

If the plan's Pages table contains 2+ rows, first stamp every target, so the gate after the workers can tell a page a worker wrote from one an earlier attempt left there (continue only on "ok":true). Here and below, <filename> and [filename] are each page's File value from the plan without its .tsx extension (candidate-tracker.tsx gives candidate-tracker):

node "${PLUGIN_ROOT}/scripts/genpage-worker-output.js" --stamp --file '<working-dir>/<filename>.tsx'

Then invoke a genpage-page-builder agent via the Task tool per page. Fire all invocations in a single message for parallel execution.

For each page, pass a prompt that includes:

  • Page name (e.g., "Candidate Tracker")
  • Target file name (e.g., "candidate-tracker.tsx")
  • Absolute path to genpage-plan.md
  • Data mode (see below) — either a RuntimeTypes path or an explicit mock flag
  • Connectors: none or <n> binding(s) — the Phase 4.5 binding-count outcome, verbatim
  • Telemetry: enabled or disabled — the Phase 4.7 probe result, verbatim
  • Working directory
  • Plugin root: ${PLUGIN_ROOT}

For Dataverse pages, include the RuntimeTypes line:

You are the genpage-page-builder agent. Generate the [Page Name] page.

  • Target file: [filename].tsx
  • Plan document: [absolute path to genpage-plan.md]
  • Data mode: dataverse
  • Connectors: [none|<n> binding(s) from Phase 4.5]
  • Telemetry: [enabled|disabled from Phase 4.7]
  • RuntimeTypes: [absolute path to RuntimeTypes.ts]
  • Working directory: [absolute path from Phase 0]
  • Plugin root: ${PLUGIN_ROOT}

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

For mock data pages, omit the RuntimeTypes line and set Data mode: mock:

You are the genpage-page-builder agent. Generate the [Page Name] page.

  • Target file: [filename].tsx
  • Plan document: [absolute path to genpage-plan.md]
  • Data mode: mock
  • Connectors: [none|<n> binding(s) from Phase 4.5]
  • Telemetry: [enabled|disabled from Phase 4.7]
  • Working directory: [absolute path from Phase 0]
  • Plugin root: ${PLUGIN_ROOT}

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

Wait for all page-builder tasks to complete before proceeding.

After the parallel workers return, validate every target file with the deterministic completeness gate:

node "${PLUGIN_ROOT}/scripts/genpage-worker-output.js" --file '<working-dir>/<filename>.tsx'

The script reuses the source-literals checks for a complete default export (a file cut off inside its own export line fails), unbalanced brackets, and a file that stops mid-statement (inside JSX, a string or a comment, or after an operator), and also rejects a markdown code fence around the code and elided code — a FIXME comment, a TODO that opens a comment or takes a colon, a comment opening with ..., "omitted for brevity", or a bare ... line. The same words in strings or JSX text ("Loading…"), or as prose in a comment ("the todo list"), are UI copy and pass. It also refuses a page that is exactly as its dispatch stamp recorded it: the worker wrote nothing, and what is there is an earlier attempt's. If a worker reported missing declared file/process tools, produced no file, or genpage-worker-output.js returns "ok":false, do not re-dispatch that worker. Run the Phase 5b page-builder workflow inline for only the failed page, preserving the same plan and dispatch inputs. This is the same inline fallback path for missing and invalid worker output. Then Grep every generated page with ['"]?borderWidth['"]?\s*: and replace the unsupported Griffel shorthand before Phase 6.

Phase 6: Deploy

For each .tsx file produced, deploy to Power Apps.

If Phase 4.5 wrote <working-dir>/connectors.json, first pre-flight the active PAC CLI:

pac model genpage upload --help

The help output must contain --connectors. If it does not, stop and surface: "connector deploy requires a pac build with pac model genpage upload --connectors — build from PowerPlatform-Scale-AdminTools or update pac." Do not silently drop bindings.

Connector deployment matrix:

  • Create (new page): include --connectors '<working-dir>/connectors.json' with the first upload --add-to-sitemap.
  • Edit — connectors changed, added, or one removed: write the full desired binding set to connectors.json and include --connectors with upload --page-id '<id>' (full replace).
  • Edit — no connector change: omit --connectors; pac preserves existing bindings. Never pass a stale or empty file on an unrelated edit.
  • Delete all connectors: write [] to connectors.json and pass --connectors so pac clears the page's connectorBindings.

If Phase 4.6 wrote <working-dir>/actions.json, pre-flight the same way — the upload --help must contain --actions; if not, stop and surface "Custom API deploy requires a pac build with pac model genpage upload --actions (PowerPlatform-Scale-AdminTools)." Don't silently drop bindings.

Custom API deployment follows the identical matrix as connectors, substituting --actions '<working-dir>/actions.json' for --connectors: pass it on create; on an edit only when bindings changed/added/removed (full replace); omit it on an unrelated edit (pac preserves existing); write [] and pass it to clear all actionBindings.

Deploy through scripts/genpage-upload.js, never by composing a raw pac model genpage upload command. The script is a thin wrapper over the same upload path /app-builder uses: it hands the prompt and agent-message to pac by file (--prompt-file/--agent-message-file). A prompt is arbitrary user text — quotes, newlines, %VAR%, &, |, non-ASCII — and putting it on a command line means the shell gets to reinterpret it. That failed live with:

Error: Not a valid command.
Parse failed on: Inspection
Was it quote wrapped? No, be sure to wrap values that contain spaces.

…for a prompt containing an ASCII-quoted multiword page name. Never "fix" that by editing the approved prompt (for example swapping in typographic quotes): the page would then be built from text the user never approved.

Write the prompt, the agent-message and, on a create, the page's display name to files first, then pass the paths. The display name is the maker's text too: substituted into a double-quoted --name value, PowerShell expanded $(…) in it before the script ran, and Revenue $100 arrived as Revenue . Pass it with --name-file, never --name.

Write these files with your file-writing tool (Write), never with a shell command. No quoting makes the maker's text safe inside a command: a single-quoted here-string ends at a line that begins with '@ (or ‘@, ’@), and the rest of that line runs. First check that none of the names is a link — a write through a link or hard link left at one of them rewrites the file it points to, outside the working directory included — and clear the files an earlier deploy left, so the tool writes each one fresh. This step carries no text:

$wd = '<working-dir>'
foreach ($f in 'prompt.txt', 'agent-message.txt', 'page-name.txt') {
  $at = Get-Item -LiteralPath (Join-Path $wd $f) -Force -ErrorAction SilentlyContinue
  if ($at -and ($at.LinkType -or $at.PSIsContainer)) { throw "$f in $wd is a link or a folder, not a file this skill wrote: remove it and re-run" }
  if ($at) { Remove-Item -LiteralPath $at.FullName -Force }
}

Then write, with the file tool, <working-dir>/prompt.txt holding the prompt, <working-dir>/agent-message.txt the agent message and, on a create, <working-dir>/page-name.txt the page's display name — each exactly its text (a trailing line break on the name is ignored). genpage-upload.js refuses any of them that is a link, a hard link or a folder, but only after the write. The name is the one value passed to pac inline. A name containing a straight double quote (") is refused before anything is uploaded — pac stores each one as \", in the page and in the navigation title it writes — so use typographic quotes (“ ”) or an apostrophe, which are stored exactly. Where pac is installed as a pac.cmd shim (Windows), a name containing % is refused too, so pick one without it.

Log the invocation into workflow-log.md under a ## Phase 6 — Deploy section before running it. Record the flags and the prompt-file path, plus the prompt's scope, so the approved text is preserved semantically without embedding arbitrary text as an executable command. Format:

## Phase 6 — Deploy
- Command: `node "${PLUGIN_ROOT}/scripts/genpage-upload.js" --env '<org-url>' --app-id '<id>' --code-file '<path>' --data-sources '<entities>' --prompt-file '<working-dir>/prompt.txt' --model '<model-id>' --name-file '<working-dir>/page-name.txt' --agent-message-file '<working-dir>/agent-message.txt' --add-to-sitemap`
- Prompt scope: full page description from plan's `## User Requirements` (create) — or the delta only (update)
- Result: page-id = <returned-id>, status = success

When present, the logged command must also include --connectors '<working-dir>/connectors.json' and/or --actions '<working-dir>/actions.json'.

Prompt semantics
  • First upload (--add-to-sitemap, no --page-id): full page description from plan's ## User Requirements.
  • Any subsequent upload (--page-id, no --add-to-sitemap): delta only — the changes in this upload, written like a commit message, never a re-statement of the original.

--add-to-sitemap is refused together with --page-id — an update cannot add a sitemap entry, and the page it names is already placed. If a create is recovered as an update after a mid-flight failure, the page is deployed but not placed, and that is reported as a failed (incomplete) deployment carrying the page id, not as success.

Applies in Phase 6 updates, Phase 6.5 PAGEREF re-uploads, Phase 7.5 fix re-deploys, and the entire edit flow.

For Dataverse entity pages (first upload — create):
node "${PLUGIN_ROOT}/scripts/genpage-upload.js" `
  --env '<org-url>' `
  --app-id '<app-id>' `
  --code-file '<working-dir>/<file>.tsx' `
  --name-file '<working-dir>/page-name.txt' `
  --data-sources 'entity1,entity2' `
  --connectors '<working-dir>/connectors.json' `
  --actions '<working-dir>/actions.json' `
  --prompt-file '<working-dir>/prompt.txt' `
  --model '<current-model-id>' `
  --agent-message-file '<working-dir>/agent-message.txt' `
  --add-to-sitemap

Omit the --connectors line when Phase 4.5 did not write connectors.json, and the --actions line when Phase 4.6 did not write actions.json.

For mock data pages: Same but omit --data-sources.

For updating existing pages (subsequent upload):

Use --page-id, omit --add-to-sitemap, and scope the prompt to the delta only:

node "${PLUGIN_ROOT}/scripts/genpage-upload.js" `
  --env '<org-url>' `
  --app-id '<app-id>' `
  --page-id '<page-id>' `
  --code-file '<working-dir>/<file>.tsx' `
  --data-sources 'entity1,entity2' `
  --connectors '<working-dir>/connectors.json' `
  --actions '<working-dir>/actions.json' `
  --prompt-file '<working-dir>/prompt.txt' `
  --model '<current-model-id>' `
  --agent-message-file '<working-dir>/agent-message.txt'

For updates, include the --connectors line only when this upload intentionally replaces or clears connector bindings; otherwise omit it to preserve the deployed page's current bindings. The same rule applies to --actions for Custom API bindings: include it only when this upload intentionally replaces or clears them.

An update without --name-file keeps the page's current display name, and one without --model its current model: pac would otherwise rename the page to its navigation title and store an empty model, so the script reads both from the deployed page and sends them again. If either cannot be read — or the name cannot be sent, because pac is a pac.cmd shim and the name holds % or " — the update still goes ahead and its result carries a warnings entry naming what may have changed; re-run with that value passed explicitly.

Phase 6.5: Navigation Fix-Up (Multi-Page Only)

Runs only when the plan has 2+ pages AND any built .tsx contains a PAGEREF_ token. Page-builders emit pageId: "PAGEREF_<filename-without-tsx>" as a placeholder because GUIDs don't exist until after Phase 6 (see Rule 13). This phase substitutes the real GUIDs.

Steps
  1. Build filename-without-tsx → page-id map from Phase 6 upload output.

  2. Sort keys by length descending so PAGEREF_pet can't match inside PAGEREF_pet-gallery.

  3. For each .tsx in <working-dir>/*.tsx (top level only, no recursion), replace every quoted "PAGEREF_<name>" (must be in double quotes — that's the format page-builders emit) with "<page-id-guid>".

  4. If a placeholder doesn't match any map key (typo, missing sibling), stop and report — never silently ship the literal string.

  5. Re-upload only the files that had at least one replacement. Use the update form of scripts/genpage-upload.js (--page-id, no --add-to-sitemap). Per the "Prompt semantics" rule in Phase 6, this is an update, so the prompt describes the delta only — not the original page description:

    Check and clear the two names as in Phase 6, then write them with the file tool: prompt.txt holding Resolve cross-page navigation placeholders to real page GUIDs (post-deploy fix-up) and agent-message.txt holding Replaced PAGEREF_<name> tokens with actual page IDs returned by Phase 6. Then:

    node "${PLUGIN_ROOT}/scripts/genpage-upload.js" `
      --env '<org-url>' `
      --app-id '<app-id>' `
      --page-id '<page-id-from-Phase-6>' `
      --code-file '<working-dir>/<file>.tsx' `
      --data-sources 'entity1,entity2' `
      --prompt-file '<working-dir>/prompt.txt' `
      --model '<current-model-id>' `
      --agent-message-file '<working-dir>/agent-message.txt'
    

Pages with no PAGEREF_ strings need no second upload.

Phase 6.7: Solution Packaging (ALM, optional)

Runs only when the plan's ## Solution Packaging has Package into solution: true. Adds the deployed app, the GenPage(s), and any connection references to the target solution so they travel cross-environment.

  1. Ensure the solution exists — create it only if it doesn't already exist: node "${PLUGIN_ROOT}/scripts/provision-solution.js" '<envUrl>' '<solutionUniqueName>' '<Friendly Name>' [--publisher '<uniqueName>'] It prints { "ok": true, "solutionId": …, "uniqueName": …, "publisherPrefix": … }; uniqueName must start with a letter and contain only letters, digits, and underscores. Without --publisher it resolves the environment's default publisher.
  2. Add the app + GenPage(s) + connection references (pass the page-id(s) returned by Phase 6 as --page-ids — the GenPage is added explicitly, it does NOT travel with the app on its own): node "${PLUGIN_ROOT}/scripts/add-page-to-solution.js" '<envUrl>' '<solutionUniqueName>' '<app-id>' --page-ids '<page-id1,page-id2>' --connection-refs '<logicalName1,logicalName2>'
  3. Log the command + result to workflow-log.md.

Cross-env note: the app (80) pulls the sitemap (62); the GenPage uxagentproject is added explicitly and pulls its uxagentprojectfile rows (including config.json with connectorBindings); each connectionreference is added so bindings resolve. The script discovers both custom-table component types from EntityDefinitions(...).ObjectTypeCode in the target environment — their numeric values are environment-specific and must never be hardcoded. At import the deployer supplies env-specific ConnectionId per connection reference via pac solution create-settings + pac solution import --settings-file.

Custom API bindings need no extra ALM step: config.json's actionBindings travels automatically in the uxagentprojectfile rows already pulled with the GenPage. The referenced Custom APIs are a separate deployment prerequisite (bound by name), not added here.

Phase 7: Verify in Browser (Optional)

After successful deployment, ask the user via AskUserQuestion:

"Would you like to verify the page(s) in the browser using Playwright?"

Options: Yes, verify in browser / Skip verification

  • If the user picks Skip verification → jump to Phase 8.
  • If the user picks Yes → read ${PLUGIN_ROOT}/skills/genpage/verify-flow.md for the full Playwright verification workflow (navigate, structural verification including below-the-fold, interactive testing, screenshots, fix-and-redeploy). The orchestrator only loads that file on demand to keep context lean when verification is skipped.
Phase 8: Summary

By Phase 8 the workflow-log.md should already contain Phase 0 through Phase 7 sections written incrementally — the planner writes Phase 1 inside its agent context, you (the orchestrator) write Phase 0 / 0.5 / 3 / 4 / 6 / 6.5 / 7 as each runs, and the entity-builder and page-builder agents append their own Phase 2 / 5 sections when invoked.

In Phase 8, append a final ## Phase 8 — Summary section to the same file:

## Phase 8 — Summary

| Page | File | Entities | Status |
|------|------|----------|--------|
| <Name> | <file>.tsx | <entities or "mock data"> | Deployed |

- App: <name> (<app-id>)
- Entities created: <list, or "none">
- Browser verification: <skipped | confirmed | failed: <reason>>

The log MUST contain command-level entries for every prereq / auth / question / upload / script invocation — not just outcome summaries. The eval harness greps the log for tokens like node --version, pac auth list, AskUserQuestion, EnterPlanMode, --prompt, check-auth.js, etc. A decision-only log (e.g., Decision: new page without the underlying AskUserQuestion) will fail Layer 1 assertions even when the agent's behavior was correct.

Then present a final summary to the user:

## Genpage Complete

| Page | File | Entities | Status |
|------|------|----------|--------|
| [Name] | [file].tsx | [entities or "mock data"] | Deployed |

App: [app name] ([app-id])
Screenshots: [if verification was done]
Next steps: Share with team, iterate on design, create additional pages

Edit Flow

For the edit flow (triggered when the genpage-planner returns { "action": "edit" }), see edit-flow.md in this folder.

The edit flow has its own 8 phases (Edit Phase 1-8): discover and select target app + page via pac model list + pac model genpage list, download, generate RuntimeTypes if needed, invoke genpage-edit-planner, apply the edit inline, deploy, verify, summarize.

1---
2name: genpage
3version: 2.3.1
4description: Creates, updates, and deploys Power Apps generative pages for model-driven apps using React v17, TypeScript, and Fluent UI V9. Orchestrates specialist agents for planning, entity creation, and code generation. Use it when user asks to build, retrieve, or update a page in an existing Microsoft Power Apps model-driven app. Use it when user mentions "generative page", "page in a model-driven", or "genux". This skill stands alone and does not require /app-builder — but if the user wants a whole app built (tables, forms, views, sitemap) rather than pages for an app that already exists, use /app-builder instead.
5author: Microsoft Corporation
6argument-hint: "<page description> | edit"
7user-invocable: true
8model: sonnet
9allowed-tools: Read, Write, Edit, Bash, Glob, Grep, WebFetch, Task, AskUserQuestion, EnterPlanMode, ExitPlanMode, TaskCreate, TaskUpdate, TaskList, read, edit, execute, search, web, agent, todo
10---
11 
12> **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding.
13 
14# Power Apps Generative Pages Builder
15 
16**Triggers:** genpage, generative page, create genpage, genux page, build genux, power apps page, model page
17**Keywords:** power apps, generative pages, genux, model-driven, dataverse, react, fluent ui, pac cli
18**Aliases:** /genpage, /gen-page, /genux
19 
20## Overview
21 
22This skill orchestrates specialist agents across the create and edit flows:
23 
24**Create flow:**
251. **`genpage-planner`** — validates prerequisites, gathers requirements, detects what
26 entities and apps exist, and returns a proposed plan for the orchestrator to present.
27 Writes `genpage-plan.md` once the orchestrator re-invokes it with the approval outcome.
28 May pause and return `connector_discovery_required` (see 2).
292. **`genpage-connector-builder`** — top-level orchestrator dispatch for connector
30 feature-gating and discovery; writes `connector-bindings.md` + `connectors.json`.
31 Runs **after** the planner has resolved create-vs-edit and the target environment
32 (discovery is mutating), then the planner is re-invoked with its contract.
33 **`genpage-customapi-builder`** — the same top-level dispatch for server-side
34 Custom API (Action/Function) needs; writes `custom-api-bindings.md` + `actions.json`.
353. **`genpage-entity-builder`** — creates Dataverse entities (tables, columns,
36 relationships, choices, sample data) via the plugin's Node.js Web API scripts
374. **`genpage-page-builder`** — generates one complete `.tsx` file per page; multiple
38 builders run in parallel for multi-page requests
39 
40**Edit flow:**
41 
425. **`genpage-connector-builder`** — top-level orchestrator dispatch when an edit adds,
43 replaces, discovers, removes, or clears connector bindings; preserves unchanged bindings
44 when the edit does not touch them.
45 **`genpage-customapi-builder`** — the same top-level dispatch when an edit adds, replaces,
46 discovers, removes, or clears Custom API bindings.
476. **`genpage-edit-planner`** — reads the downloaded page artifacts, gathers change
48 requirements, presents an edit plan, writes `genpage-edit-plan.md`
49 
50You (the skill) coordinate the agents and own connector and Custom API dispatch, app
51creation, RuntimeTypes generation, deployment, browser verification, and the inline
52application of planned edits.
53 
54## References
55 
56- **Code generation rules**: [rules.md](../../references/rules.md)
57- **Troubleshooting**: [troubleshooting.md](../../references/troubleshooting.md)
58- **Sample pages**: [samples/](../../samples/)
59 
60## Development Standards
61 
62- **React 17 + TypeScript** — all generated code
63- **Fluent UI V9** — `@fluentui/react-components` exclusively (DatePicker from `@fluentui/react-datepicker-compat`, TimePicker from `@fluentui/react-timepicker-compat`)
64- **Single file architecture** — all components, utilities, styles in one `.tsx` file
65- **No external libraries** — only React, Fluent UI V9, approved Fluent icons, D3.js for charts
66- **Type-safe DataAPI** — use RuntimeTypes when Dataverse entities are involved
67- **Responsive design** — flexbox, relative units, never `100vh`/`100vw`
68- **Accessibility** — WCAG AA, ARIA labels, keyboard navigation, semantic HTML
69- **Complete code** — no placeholders, TODOs, or ellipses in final output
70 
71---
72 
73## Instructions
74 
75Follow these phases in order for every `/genpage` invocation.
76 
77**Every `<…>` you fill into a command goes in single quotes** (`'<working-dir>/prompt.txt'`, `'<App Name>'`).
78PowerShell and bash expand nothing inside them — not `$name`, not `$(…)`, not a backtick — and a space does not split
79the value. Escape a single quote inside the value the shell's way: in PowerShell double it, the typographic `‘ ’`
80included (`'Bob''s app'`); in bash close, escape and reopen (`'Bob'\''s app'`). Never use double quotes for a value
81(`"Revenue $100"` arrived as `Revenue `) and never leave one bare (`D:\Work Projects\…` became two arguments). A value
82holding a double quote `"` is not put on a command line at all — Windows PowerShell 5.1 drops it and can split the
83argument there — so ask for an app or solution name without one. Free text (a prompt, an agent message, a page's
84display name) never goes into a shell command at all, however it is quoted: even a single-quoted here-string ends at a
85line that begins with `'@`, and the rest of that line runs. Write it with your file-writing tool and pass the path
86(Phase 6).
87 
88### Phase 0: Create Working Directory
89 
90Derive a short folder name from the user's requirements:
91 
921. Extract the page name or a 2-4 word summary from `$ARGUMENTS`
932. Convert to kebab-case (e.g., "Candidate Tracker" → `candidate-tracker`)
943. Create the folder: `mkdir -p '<folder-name>'` (PowerShell or bash, the shells every command in this skill is
95 written for)
964. Resolve its absolute path — this is the **working directory** for all subsequent phases
97 
98### Phase 0.5: Initialize Local-Dev Manifest
99 
100Write `package.json` and `genpage.d.ts` into the working directory so the
101developer can `npm install` and get IntelliSense, type-checking, and "go to
102definition" in their editor. Versions come from
103`references/supported-dependencies.md` (single source of truth:
104`scripts/lib/supported-dependencies.js`).
105 
106```bash
107node "${PLUGIN_ROOT}/scripts/generate-page-manifest.js" '<working-dir>' '<kebab-slug>'
108```
109 
110- `<kebab-slug>` is the same slug used for the working directory.
111- Add `--features charts,datepicker,timepicker` (comma-separated) only when
112 the requirements clearly call for them; otherwise omit and keep the
113 manifest lean.
114- The script keeps an existing `package.json` (no `--force`) only when it
115 already lists every package the requested features need. A package present
116 at a version the user changed is kept and listed as `versionDrift` in the
117 JSON summary — mention it, since local type-checking then differs from the
118 versions pages are written for. Pass `--force` to overwrite (used in
119 regeneration flows when versions drift).
120- Output is a JSON summary on stdout. **Continue only on exit 0.**
121 - **Exit 2 — halt** and show the user the error line. When it names
122 packages a requested feature needs that the existing `package.json`
123 lacks, ask whether they will merge them into it or want a rerun with
124 `--force` (which replaces the file), and rerun until it exits 0 before
125 Phase 1 — continuing would leave a `--features charts` run on a manifest
126 without `d3`, the stale state this check exists to stop. Exit 2 also
127 reports a working directory that is a link or not a directory: never
128 write through it.
129 - Any other non-zero exit is a usage error in the command above: fix it and
130 rerun.
131 
132### Phase 1: Plan
133 
134> **⚠️ CRITICAL — the interactive steps run HERE, in the main conversation loop.
135> You MUST NOT dispatch them to a `Task` subagent.**
136>
137> A `Task` subagent is **headless**: `AskUserQuestion`, `EnterPlanMode` and
138> `ExitPlanMode` never reach the user from inside one. A flow that specifies them
139> there cannot complete — the question is never answered and the approval never
140> given. This is the same rule `/app-builder` follows, and the reason its
141> subagents are headless workers only.
142>
143> `genpage-planner` is therefore a **headless discovery agent**. It runs the
144> read-only work and returns what it found; **you** ask the questions and present
145> the plan.
146>
147> Run in the main loop (never delegated):
148> 1. Prerequisite validation (`node --version`, `pac help` version > 2.10.0)
149> 2. Auth verification (`pac auth list`, environment selection)
150> 3. The structured "Create new / Edit existing" question (`AskUserQuestion`)
151> 4. Language detection (`pac model list-languages`) — only on new-page path
152> 5. Entity existence detection (`pac model list-tables --search`)
153> 6. App detection (`pac model list`) with proper selection prompts
154> 7. Plan-mode presentation and approval (`EnterPlanMode` / `ExitPlanMode`)
155> 8. Telling `genpage-planner` the approval outcome — it writes `genpage-plan.md`
156> itself — and confirming the file exists before Phase 2
157>
158> Steps 1, 2, 4, 5, 6 are read-only discovery and **may** be delegated to
159> `genpage-planner`; steps 3, 7 and 8 never can. Delegating the discovery is an
160> optimisation, not a requirement — running it inline is equally correct, but
161> `genpage-plan.md` is still written by the planner either way (see step 6 of the
162> Steps list): its section headings are a machine-readable contract every
163> downstream phase parses by name, so it has exactly one author. If you ran the
164> discovery inline, dispatch the planner once with what you found, so it has the
165> context to produce the plan without repeating your reads.
166>
167> **Never skip the prereq/auth steps**, even when `$ARGUMENTS` already states the
168> intent. A stated intent lets you skip *question 3*; it does not establish that
169> the CLI is present, authenticated, or pointed at the right environment.
170>
171> **Whoever runs a step records it in `workflow-log.md`** in the documented
172> format — `AskUserQuestion: <question> → <answer>`, `EnterPlanMode called`
173> followed by the response. The log is the contract the eval harness reads, and
174> it does not care which loop made the call.
175 
176#### Unattended runs (Copilot autopilot / Claude auto-accept)
177 
178Copilot CLI autopilot and Claude Code auto-accept drive this skill with **no user
179watching**. Every gate below is written as "ask the user", and in those modes
180there is nobody to answer: the run either stalls on a question no one sees or,
181worse, records an answer nobody gave. Resolve the mode **once, at the start of
182Phase 1**, and carry it through every gate:
183 
184```bash
185node "${PLUGIN_ROOT}/scripts/resolve-interaction-mode.js"
186```
187 
188One JSON line, always exit 0 — "there is no user" is a fact about the run, not a
189failure of it:
190 
191```json
192{ "ok": true, "interactive": false, "reason": "POWER_PLATFORM_SKILLS_NONINTERACTIVE is set" }
193```
194 
195A run is unattended when `--non-interactive` is passed or
196`POWER_PLATFORM_SKILLS_NONINTERACTIVE` is `1`/`true` — the same switch
197`/app-builder` already uses, so one setting covers both skills.
198 
199When `interactive` is `false`, do not call `AskUserQuestion`, `EnterPlanMode` or
200`ExitPlanMode` at all. Take the documented default and record it in
201`workflow-log.md` as `Unattended default: <question> → <answer> (<reason>)`, so
202the log still shows what decided the run:
203 
204| Gate | Attended | Unattended |
205| --- | --- | --- |
206| Recording the resolved mode | Nothing to record | Write the mode itself as `Unattended default: interaction mode → unattended (<reason>)` before the first gate. Record it with this marker, not as free prose such as `Interaction mode: unattended` — the evaluator keys on the marker, and prose forms are indistinguishable from an attended log that merely mentions the word (`Mode: unattended = false`, `not unattended`). |
207| Create new / edit existing (step 2) | `AskUserQuestion` | Whatever `$ARGUMENTS` states. With nothing stated, **create new** — the only additive choice. |
208| An agent returns `needs_input` (step 4) | Ask, then re-invoke | Re-invoke with the option the agent marked `"default": true`. If it marked none, **halt**. |
209| Plan approval (step 5) | `EnterPlanMode` / `ExitPlanMode` | Treat the plan as approved and continue to step 6, which still writes `genpage-plan.md` through the planner. The plan is recorded, just not presented. Log this gate as `Unattended default: plan approval → approved (<reason>)` — the evaluator looks for the plan/approval wording and `approved` on that one line, so a paraphrase such as `→ auto-approve` is read as a missing approval record. |
210| Browser verification (Phase 7) | Offer it | Skip it. |
211 
212**Suppressing a prompt never authorizes destructive work.** Editing an existing
213page overwrites source nobody reviewed, so on the **edit** path an unattended run
214requires the page to be named explicitly in `$ARGUMENTS`. Do not infer the target
215from a search result and do not fall back to "the only page that matched". If the
216target is ambiguous, **halt and say so** — an unattended run that guesses which
217page to overwrite is the one failure this table exists to prevent.
218 
219Halting is a normal outcome here, not an error to route around: report what was
220missing and stop, so the run can be re-driven with the decision supplied.
221 
222#### Steps
223 
2241. Run the prerequisite, auth and discovery steps (inline, or via
225 `genpage-planner` as a headless worker). Connector discovery has **not** run
226 yet, so the contract is the literal `No connector bindings.`, with discovery
227 available on request (see 1a). Custom API discovery is likewise orchestrator-
228 owned and has not run either, so its contract starts as the literal
229 `No custom API bindings.`, with discovery available on request (see 1b).
2302. Ask question 3 (**create new / edit existing**) with `AskUserQuestion`, unless
231 `$ARGUMENTS` already settles it. On **edit**, jump to the **Edit Flow** section.
2323. If discovery reports `{ "action": "connector_discovery_required" }`, invoke
233 `genpage-connector-builder` with the intent — **Mode: `create`** for a new
234 page, **Mode: `edit`** for an edit — using the resolved environment URL, then
235 re-run discovery with the builder's `## Connector Bindings` contract and
236 `connectors.json` status. If it instead reports
237 `{ "action": "custom_api_discovery_required" }`, invoke `genpage-customapi-builder`
238 the same way (same mode, resolved environment URL, plus the returned `pageTables`),
239 then re-run the planner with the builder's `## Custom API Bindings` contract and
240 `actions.json` status (see 1b).
241 If `genpage-connector-builder` or `genpage-customapi-builder` instead reports
242 that its declared file or process-execution tools are unavailable, do **not**
243 retry the same worker. Record the worker failure, read that worker's agent
244 file, and run the discovery-builder workflow inline in this orchestrator
245 using the same resolved mode, environment, intent, and safety gates. This
246 inline fallback is recovery from task-runtime tool exposure only; it does not
247 bypass connector/custom-API feature gates or user decisions.
2484. If any agent returns `{ "action": "needs_input", … }`, ask its questions here
249 with `AskUserQuestion`, record them in `workflow-log.md`, and re-invoke that
250 agent with the answers. Agents never prompt; they request.
2515. Present the plan with `EnterPlanMode` and get approval via `ExitPlanMode`.
252 On a revision request, re-invoke the planner with the requested revisions and
253 present the revised plan again.
2546. **On approval, re-invoke `genpage-planner` with the approval outcome _plus
255 the plan body it returned and everything it already discovered_.** The planner
256 writes `genpage-plan.md` in its own final step, and it only reaches that step
257 when it is told the plan was approved — a `Task` subagent is headless, so it
258 cannot see the `ExitPlanMode` result any other way. A re-invocation is a fresh
259 run with no memory of the last one: carry the state forward or it will re-ask
260 questions the user has already answered, or re-derive a plan that is not the
261 one they approved. Same rule as Phase 2b. Do not write the file yourself — its
262 section headings are a machine-readable contract that every downstream phase
263 parses by name.
264 
265 Before that approval writeback dispatch, quarantine any previous authoritative
266 plan so a stale file cannot satisfy the post-dispatch existence check:
267 
268 ```powershell
269 node "${PLUGIN_ROOT}/scripts/genpage-plan-provenance.js" prepare --plan '<working-dir>/genpage-plan.md'
270 ```
271 
272 Continue only on `"ok":true`. It refuses a plan path, approval sidecar
273 (`.approved-genpage-plan.md`) or `.genpage-provenance` folder that is a link or
274 junction (a dangling one included) or the wrong kind of entry, since the approved
275 plan would be written through it: on `"ok":false`, halt and tell the user to remove
276 what the error names.
277 
278 Also save the plan body the planner returned for approval — the one presented
279 with `EnterPlanMode`, or approved by default when unattended — to a sidecar such
280 as `<working-dir>/.approved-genpage-plan.md` (not `genpage-plan.md`), exactly as
281 returned. The planner writes a different document from it (the schema file, with
282 suffix-only names), so the verifier compares what both name as targets: the pages
283 to build.
284 
285 If `genpage-planner` reports that the file tools needed to write the approved
286 `genpage-plan.md` are unavailable, do not retry it and do not write the plan
287 inline. **Halt** with the approved plan body and failure recorded. Planner
288 authorship is the provenance gate for every downstream phase.
2897. Confirm `<working-dir>/genpage-plan.md` exists before starting Phase 2.
290 Reaching Phase 2 without it means building from a plan nobody approved, and
291 Phase 2 reads that file as its first action.
292 
293 **If it is missing, do not proceed and do not write it yourself.** Re-invoke
294 the planner once more with the approval outcome and the plan body after running
295 the `prepare` command above again. If it is still missing, stop and tell the
296 user what was approved and what failed to be written — a hand-written substitute
297 is a plan with no provenance, and every later phase will treat it as approved.
298 
299 If it exists, verify its provenance before Phase 2:
300 
301 ```powershell
302 node "${PLUGIN_ROOT}/scripts/genpage-plan-provenance.js" verify --plan '<working-dir>/genpage-plan.md' --approved '@<working-dir>/.approved-genpage-plan.md'
303 ```
304 
305 Continue only when the JSON result has `"ok":true`, and record its `writtenHash`
306 in `workflow-log.md`. If the written plan targets other pages than the approved
307 plan named — an extra, missing or renamed page file — halt: downstream phases
308 would build pages the user did not approve.
309 
310#### 1a. Connector discovery is orchestrator-owned and never speculative
311 
312`genpage-connector-builder` is dispatched only by this top-level orchestrator,
313not by `genpage-planner`. This keeps connector discovery in one agent while avoiding
314nested `Task` calls from the planner.
315 
316**Never run discovery before the planner returns** — not even when `$ARGUMENTS`
317obviously mentions SharePoint, Teams, Office 365 or a custom REST source.
318Discovery is a **mutating** operation: it can create a connection reference. The
319planner is what resolves (a) create vs. edit and (b) which environment, and it may
320resolve either differently from the active `pac auth` profile. A connection
321reference created in the wrong environment, or in create mode for what turns out
322to be an edit, **cannot be undone** by discarding the local outputs.
323 
324So the sequence is always: plan first, then discover, then re-plan.
325 
326- Every first planner invocation gets the literal contract `No connector bindings.`
327 and is told discovery has not run.
328- When the planner determines connector-backed data is needed — from `$ARGUMENTS`
329 or from its own user clarification — it returns
330 `{ "action": "connector_discovery_required", "intent": "..." }` **together with
331 the resolved action and environment URL**.
332- Only then dispatch `genpage-connector-builder` with that mode, that environment
333 URL, the working directory and `${PLUGIN_ROOT}`. Read
334 `<working-dir>/connector-bindings.md` and verify `<working-dir>/connectors.json`
335 is a bare JSON array, then re-run the planner with the refreshed contract.
336 
337The builder remains the single owner of connector discovery: it writes
338`No connector bindings.` + `[]` when the page needs no connector, and performs all
339connection discovery only when one is required.
340 
341#### 1b. Custom API discovery is orchestrator-owned too
342 
343`genpage-customapi-builder` is likewise dispatched only by this top-level orchestrator,
344not by `genpage-planner` — the planner has no `Task` tool, and the builder may need to ask
345the user which Custom API to bind, which only the main loop can do. Custom API discovery is
346**read-only** (a Web API query over the Custom API tables), so unlike connector discovery it
347carries no "wrong environment / wrong mode cannot be undone" hazard. It still runs after the
348planner so it targets the environment the planner resolved and binds to the page tables it
349detected.
350 
351- Every first planner invocation gets the literal contract `No custom API bindings.` and is
352 told discovery has not run.
353- When the planner determines a server-side Custom API (Action/Function) is needed — from
354 `$ARGUMENTS` or from its own user clarification — it returns
355 `{ "action": "custom_api_discovery_required", "intent": "...", "resolvedAction": "create",
356 "envUrl": "...", "pageTables": "..." }`.
357- Only then dispatch `genpage-customapi-builder` with that mode, environment URL, page tables,
358 the working directory and `${PLUGIN_ROOT}`. Read `<working-dir>/custom-api-bindings.md` and
359 verify `<working-dir>/actions.json` is a bare JSON array, then re-run the planner with the
360 refreshed contract.
361 
362The builder remains the single owner of the `custom-api` feature gate: it probes first, writes
363`No custom API bindings.` + `[]` when the gate is off or the page needs no Custom API, and
364performs discovery only when one is required.
365 
366#### Invocation prompt
367 
368Pass a prompt that includes:
369 
370- The user's requirements: `$ARGUMENTS`
371- The working directory (absolute path from Phase 0)
372- The plugin root path: `${PLUGIN_ROOT}`
373- The connector contract: the full body of `<working-dir>/connector-bindings.md`,
374 or the literal `No connector bindings.` when discovery was not needed
375- The connector upload file status: `<working-dir>/connectors.json` exists and is
376 a bare JSON array, or `no connectors.json; omit --connectors`
377- The Custom API contract: the full body of `<working-dir>/custom-api-bindings.md`,
378 or the literal `No custom API bindings.` when discovery was not needed
379- The Custom API upload file status: `<working-dir>/actions.json` exists and is
380 a bare JSON array, or `no actions.json; omit --actions`
381 
382Example:
383 
384> You are the genpage-planner agent. Plan generative page(s) for the following requirements:
385>
386> [paste $ARGUMENTS here verbatim, or "no arguments provided — gather from user"]
387>
388> Working directory: [absolute path from Phase 0]
389> Plugin root: ${PLUGIN_ROOT}
390>
391> Connector discovery is orchestrator-owned. Do **not** invoke
392> `genpage-connector-builder` from inside the planner. The `----- BEGIN/END
393> CONNECTOR BINDINGS -----` lines below are delimiters for **this prompt only**:
394> they mark where the contract starts and ends. Do **not** copy them into
395> `genpage-plan.md`. The `## Connector Bindings` section of the plan is exactly
396> the text between them:
397>
398> ----- BEGIN CONNECTOR BINDINGS -----
399> [paste connector-bindings.md body, or `No connector bindings.`]
400> ----- END CONNECTOR BINDINGS -----
401>
402> Connector upload file (orchestration metadata; not part of the
403> `## Connector Bindings` section): [absolute path to connectors.json, or
404> `none — omit --connectors`]
405>
406> If your clarification questions reveal connector-backed data that is not covered
407> by the connector contract above, stop and return
408> `{ "action": "connector_discovery_required", "intent": "<connector need>",
409> "resolvedAction": "create" | "edit", "envUrl": "<the environment you resolved>" }`
410> instead of trying to discover connectors yourself. `resolvedAction` and `envUrl`
411> are required — discovery is dispatched against exactly those.
412>
413> Custom API discovery is orchestrator-owned too. Do **not** invoke
414> `genpage-customapi-builder` from inside the planner. The `----- BEGIN/END
415> CUSTOM API BINDINGS -----` lines below are delimiters for **this prompt only**:
416> they mark where the contract starts and ends. Do **not** copy them into
417> `genpage-plan.md`. The `## Custom API Bindings` section of the plan is exactly
418> the text between them:
419>
420> ----- BEGIN CUSTOM API BINDINGS -----
421> [paste custom-api-bindings.md body, or `No custom API bindings.`]
422> ----- END CUSTOM API BINDINGS -----
423>
424> Custom API upload file (orchestration metadata; not part of the
425> `## Custom API Bindings` section): [absolute path to actions.json, or
426> `none — omit --actions`]
427>
428> If your clarification questions reveal a server-side Custom API (Action/Function)
429> not covered by the Custom API contract above, stop and return
430> `{ "action": "custom_api_discovery_required", "intent": "<operation>",
431> "resolvedAction": "create" | "edit", "envUrl": "<the environment you resolved>",
432> "pageTables": "<page tables or none>" }` instead of trying to discover it yourself.
433>
434> Follow the instructions in your agent file. Validate prereqs and confirm auth.
435> The create/edit decision and the resolved environment are supplied to you by the
436> orchestrator (it asks; you are headless) — use them rather than prompting. If you
437> need any further decision, return `{ "action": "needs_input", … }`. Write
438> genpage-plan.md to the working directory once the orchestrator reports the plan
439> approved. Return the page list, entity status, app selection, and any
440> `{ "action": "edit" }` signal when complete.
441 
442### Phase 2: Create Entities (Conditional)
443 
444Read `genpage-plan.md` from the working directory. Check the **Entity Creation Required**
445section.
446 
447**If the section literally says "No entity creation required — all entities already exist":**
448Skip to Phase 3.
449 
450**If entities need creating:**
451 
452#### 2a. Pre-flight: az + pac + Dataverse
453 
454Entity creation runs through the plugin's Node.js Web API scripts using `az` for
455auth, and the `az` and `pac` identities should normally match. Run the
456consolidated pre-flight:
457 
458```bash
459node "${PLUGIN_ROOT}/scripts/check-auth.js" --require-pac
460```
461 
462Genpage deploys pages via `pac model genpage`, so pass `--require-pac` to keep a missing pac login
463a hard blocker (the app-builder skill omits the flag — its build path only needs the az token).
464It returns a single JSON object:
465 
466```json
467{
468 "ok": true | false,
469 "blocker": null | "usage" | "az_missing" | "az_not_logged_in" | "az_timeout" | "pac_not_logged_in"
470 | "pac_timeout" | "no_env_url" | "whoami_403" | "whoami_401" | "whoami_error",
471 "message": "human-readable next step",
472 "warnings": ["..."],
473 "azUser": "...", "pacUser": "...", "envUrl": "...",
474 "identitiesMatch": true | false,
475 "whoAmI": { "ok": true, "userId": "...", "organizationId": "..." }
476}
477```
478 
479- **`ok: true` and `identitiesMatch: true`** → proceed to 2b.
480- **`ok: true` and `identitiesMatch: false`** → proceed to 2b but surface the
481 `message` to the user as an inline warning ("az is X, pac is Y — WhoAmI works
482 for now, but if entity creation later returns 403, run the suggested
483 `az login --username` to align them").
484- **`ok: false`** → show the `message` field to the user verbatim and
485 **stop the workflow**. The script already includes a fix-it command for every
486 blocker (run `az login`, etc.).
487- **`blocker: "usage"`** is the one exception: the `check-auth.js` command line itself was wrong (a
488 mistyped flag or a missing value). Fix the invocation and run it again instead of stopping.
489- **`blocker: "az_timeout"`** means the Azure CLI was too slow to answer, not that it is missing or
490 signed out. Retry once; if it recurs on a busy machine, set `POWER_PLATFORM_SKILLS_AZ_TIMEOUT_MS`
491 (milliseconds, default 60000) for the Azure CLI budget.
492- **`blocker: "pac_timeout"`** means `pac org who` did not answer within its fixed 60 s, so the PAC
493 login is unknown, not missing. Retry once. The Azure CLI setting above does not change this budget.
494 
495Capture `envUrl` from the result — Phase 2b passes it to the entity-builder.
496 
497#### 2b. Invoke entity-builder
498 
499Invoke the `genpage-entity-builder` agent via the `Task` tool. Pass in the prompt:
500- Path to `genpage-plan.md`
501- Working directory (absolute path)
502- Plugin root: `${PLUGIN_ROOT}`
503- Dataverse env URL (from `pac org who`)
504 
505The entity-builder reads `Solution` and `Publisher Prefix` directly from the
506plan's `## Environment` — no need to re-thread them here.
507 
508Wait for completion. The builder writes a transactional log at
509`<working-dir>/genpage-entity-creation-log.md` for recovery on failure.
510 
511**The builder is headless and will ask for the sample-data decision by returning
512`{ "action": "needs_input", … }`** (it has no way to prompt). Handle it here, in
513this loop, exactly as Phase 1 step 4 does:
514 
515- Ask each question with `AskUserQuestion`.
516- Record every exchange in `workflow-log.md` as `AskUserQuestion: <question> → <answer>`.
517- Re-invoke the builder with the answers plus everything it already returned, so
518 it can carry out the step that depended on them (sample data is created by a
519 second pass of its own CLI, not by anything here). A re-invocation restarts the
520 agent at its first step, which is safe — `provision-entities.js` is idempotent
521 and re-reports the existing tables rather than recreating them — but say which
522 decision has now been answered so it goes on to the sample-data step instead of
523 asking again.
524 
525Repeat until it returns a completion rather than a request. Proceeding to Phase 3
526on a `needs_input` return silently drops the decision the user was asked to make.
527 
528### Phase 3: App Creation/Selection
529 
530Read `genpage-plan.md` for the app decision and the `Solution` line in
531`## Environment`.
532 
533**If "create new":**
534 
535```powershell
536pac model create --name '<App Name>' --solution '<Solution unique name>' --publish
537```
538 
539**`--solution` is mandatory.** `pac model create` errors out with
540`"The given solution name is not valid: ()"` if you omit it — its claimed
541"active solution" fallback does not work in practice.
542 
543**`--publish` is mandatory.** Without it the new appmodule stays in draft and
544the genux runtime URL errors with "app not published".
545 
546- Use the plan's `Solution` value verbatim. The planner always writes one
547 (default fallback is literally `Default`).
548- If the plan is somehow missing `Solution`, pass `--solution Default` —
549 every Dataverse env has a built-in "Default Solution" by that unique name.
550 
551Store the new app-id for Phase 6.
552 
553**If existing app-id:** Use it directly. `pac model create` is not called, so
554the `Solution` line is informational only for this phase.
555 
556### Phase 4: Generate RuntimeTypes (Conditional)
557 
558If any page uses Dataverse entities, generate the TypeScript schema:
559 
560```powershell
561pac model genpage generate-types --data-sources 'entity1,entity2,...' --output-file '<working-dir>/RuntimeTypes.ts'
562```
563 
564> **Windows + Bash**: Always use forward slashes in file paths (e.g., `D:/temp/RuntimeTypes.ts`).
565 
566After generating, read the RuntimeTypes.ts file to verify it generated correctly.
567 
568**For mock data pages only:** Skip this phase.
569 
570### Phase 4.5: Connector Bindings (Conditional)
571 
572Read the plan's `## Connector Bindings` section and treat it as bindings **only when
573it contains an actual binding table** (a `| Logical Name | …` header with at least
574one data row). If the section is `No connector bindings.`, empty, missing, or
575malformed, the page has no connectors: skip this phase entirely — do not create or
576pass `connectors.json`, and do not add `--connectors` on upload.
577 
578**Carry this decision into code generation.** The outcome is `Connectors: <n>
579binding(s)` or `Connectors: none` for the rest of the run, and Phase 5 **must** pass
580it verbatim in every page-builder dispatch — otherwise the generated page could call
581a connector this run never binds, and the page fails at runtime instead of simply
582omitting the feature. The dispatch value is the **binding count**, not a flag state:
583an empty binding table produces `none`, because the page-builder only ever needs to
584know how many bindings it may call.
585 
586When there are real bindings, the `genpage-connector-builder` agent already wrote
587`<working-dir>/connectors.json` during planning — verify it exists and matches the
588plan table. If it is missing, derive it from the plan table as a **bare JSON
589array** (never the `{ "connectorBindings": [...] }` object wrapper — that is the
590deployed page `config.json` shape that `pac` writes):
591 
592```json
593[
594 {
595 "logicalName": "new_uxtest_sharepoint",
596 "connectorId": "/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
597 "dataset": "https://host.sharepoint.com/sites/x",
598 "tables": ["5709dd6f-c73e-4079-ad23-2334e45e0e13"],
599 "tableDisplayNames": ["Pet"]
600 },
601 {
602 "logicalName": "new_uxtest_msnweather",
603 "connectorId": "/providers/Microsoft.PowerApps/apis/shared_msnweather",
604 "dataset": "",
605 "operations": ["CurrentWeather"]
606 }
607]
608```
609 
610Do **not** write connection IDs into `connectors.json` — the importing maker/admin
611fills env-specific `ConnectionId` values through solution deployment settings.
612 
613### Phase 4.6: Custom API Bindings (Conditional)
614 
615**Re-probe the feature gate here — do not rely on the plan content alone.** A plan
616authored while the flag was ON must not deploy Custom API bindings after it is turned OFF:
617 
618```powershell
619node "${PLUGIN_ROOT}/scripts/lib/feature-flags.js" custom-api
620```
621 
622**If it prints `disabled`:** Custom API support is OFF. Do not create or pass `actions.json`,
623and never add `--actions` on upload. The plan's `## Custom API Bindings` section still decides
624whether the run may go on: only a body of exactly `No custom API bindings.` continues, with no
625Custom APIs. For an **actual binding table** (a `| Name | Kind | …` header with at least one data
626row), **halt before page generation**. That plan was made while the flag was on, and page
627generation takes the table as permission to emit `executeAction` / `executeFunction` calls —
628deploying them with no bindings leaves pages calling Custom APIs that are not bound. Tell the user
629to turn the flag back on, or re-run planning (the Custom API builder writes no bindings while the
630flag is off). A missing, empty, or malformed section halts too, exactly as in the `enabled`
631branch. (Backstop: `list-custom-apis.js` also fails closed with exit 3 if invoked while OFF.)
632 
633**If it prints `enabled`:** read the plan's `## Custom API Bindings` section. The section is
634mandatory: continue with no actions only when its body is exactly `No custom API bindings.`.
635Treat it as bindings only when it contains an actual binding table (a `| Name | Kind | …`
636header with at least one data row). If the section is missing, empty, or malformed, **halt**
637before page generation; do not interpret a planner/schema failure as "no Custom APIs".
638 
639When there are real bindings, the `genpage-customapi-builder` agent already wrote
640`<working-dir>/actions.json` during planning — verify it exists and matches the plan table. If
641it is missing, derive it as a **bare JSON array** of
642`{ name, isFunction, boundEntityLogicalName?, displayName, parameterKinds }` entries (Action row
643`isFunction:false`, Function `true`; `boundEntityLogicalName` only for an entity-bound, non-
644`(Global)`, row) — see `${PLUGIN_ROOT}/references/custom-api.md`. Never the
645`{ "actionBindings": [...] }` object wrapper (the deployed `config.json` shape `pac` writes).
646 
647### Phase 4.7: Page Telemetry (Conditional)
648 
649**Re-probe the feature gate here — do not rely on the plan content alone.**
650 
651```powershell
652node "${PLUGIN_ROOT}/scripts/lib/feature-flags.js" custom-telemetry
653```
654 
655This phase has no bindings and no artifacts; it only decides whether page-builder is
656permitted to instrument. **If it prints `disabled`**, generated pages contain no
657telemetry calls at all — identical to before the feature existed.
658 
659**Carry this decision into code generation.** The probe result is `Telemetry:
660disabled` / `Telemetry: enabled` for the rest of the run, and Phase 5 **must** pass it
661verbatim in every page-builder dispatch.
662 
663`enabled` is permission, not instruction. Even when it is on, page-builder emits
664telemetry **only** when the maker asked to measure or track something in their own
665words; the default output is still a page with zero telemetry. See
666`${PLUGIN_ROOT}/references/page-telemetry.md`.
667 
668### Phase 5: Build Pages (Parallel)
669 
670Read `genpage-plan.md` and extract the pages table.
671 
672#### 5a. Validate the plan before dispatch
673 
674Before invoking any builders, verify:
675- At least one page exists in the `## Pages` table
676- Every page has a `### [Page Name]` subsection in `## Per-Page Specifications`
677- **All filenames in the `## Pages` table are safe and unique.** Run the deterministic gate —
678 it applies the same rule as the plan validator and `/app-builder`:
679 
680 ```powershell
681 node "${PLUGIN_ROOT}/scripts/check-page-files.js" --plan '<working-dir>/genpage-plan.md'
682 ```
683 
684 Continue only on `"ok":true`. It reads the plan's one `## Pages` table: a plan with a second
685 Pages table that has a File column is refused, not read by its first — one quoted in the
686 requirements, fenced, quoted or not, counts. It refuses absolute paths (drive-qualified ones included), `..`
687 traversal, backslash separator aliases, a character a shell would expand or split on (a space,
688 `$`, a backtick, `;` — page file names use letters, digits, `.`, `-` and `_`), a name Windows cannot store (a device name such as
689 `CON.tsx`, a reserved character such as `:`, or a trailing dot or space), a name that is not a
690 `.tsx` page file (such as `package.json` or `RuntimeTypes.ts`), a page path that is itself a link,
691 junction or hard link or is not a regular file, a parent that resolves through a link or junction
692 to outside the working directory or cannot be resolved at all (a dangling link, a folder it cannot
693 read), and case-insensitive collisions — `Page.tsx` plus `page.tsx` in the plan, two names that
694 reach one file through a link, or a name whose file or folder is already on disk under another
695 spelling. On any problem, halt and re-plan instead of rewriting filenames here: a renamed
696 file is a page the user did not approve, and the provenance check compares page files.
697 Duplicate filenames cause silent last-writer-wins data loss under parallel execution.
698 
699See `${PLUGIN_ROOT}/references/plan-schema.md` for the full contract.
700 
701#### 5b. Single-page fast path (skip Task dispatch when N=1)
702 
703**If the plan's Pages table contains exactly one row**, do NOT dispatch a Task
704subagent. Inline the page-builder workflow directly in the orchestrator:
705 
7061. Read `${PLUGIN_ROOT}/references/rules.md`
7072. Read the sample listed in the plan's `## Relevant Samples`
7083. Only when the plan's `## Connector Bindings` section contains an **actual
709 binding table** (a `| Logical Name | …` header with at least one data row),
710 also read `${PLUGIN_ROOT}/references/connectors.md`. Treat a
711 `No connector bindings.` sentinel or an empty/missing/malformed section as
712 having no connectors (same contract as Phase 4.5 and genpage-page-builder).
7133b. Only when the Phase 4.6 probe printed `enabled` **and** the plan's
714 `## Custom API Bindings` section contains an **actual binding table** (a
715 `| Name | Kind | …` header with at least one data row), also read
716 `${PLUGIN_ROOT}/references/custom-api.md`. Treat a
717 `No custom API bindings.` sentinel as no Custom APIs. If the section is
718 missing, empty, or malformed, halt before inline generation (same contract as
719 Phase 4.6); do not downgrade a planner/schema failure to "no Custom APIs."
720 (A `disabled` probe with a binding table has already halted in Phase 4.6.)
7214. If the plan's Per-Page Specification has `Needs caching: true`, also read
722 `${PLUGIN_ROOT}/references/data-caching.md`
7235. If the plan's `## Environment` indicates non-English languages, also read
724 `${PLUGIN_ROOT}/references/localization.md`
7255b. Only when the Phase 4.7 probe printed `enabled` **and** the maker's own request
726 asks to measure, track, monitor, or diagnose something, also read
727 `${PLUGIN_ROOT}/references/page-telemetry.md`. In every other case the page
728 contains no telemetry calls — do not read it.
7296. Read `genpage-plan.md` (already in working directory) and `RuntimeTypes.ts`
730 if Data mode is dataverse
7317. Stamp the target, so the gate below can tell the page you write from one an earlier attempt left
732 there. Here and below, `<filename>` is the page's `File` value from the plan without its `.tsx`
733 extension (`candidate-tracker.tsx` gives `candidate-tracker`):
734 
735 ```powershell
736 node "${PLUGIN_ROOT}/scripts/genpage-worker-output.js" --stamp --file '<working-dir>/<filename>.tsx'
737 ```
738 
739 Then write the `.tsx` file to `<working-dir>/<filename>.tsx` following all rules
7408. After writing, Grep every named import from `@fluentui/react-icons` against
741 `${PLUGIN_ROOT}/references/verified-icons.txt` (one Grep per name).
742 Rewrite any unverified names with the closest verified alternative; do not
743 load the full icon list into context
7449. Grep the generated file with `['"]?borderWidth['"]?\s*:`. Griffel rejects
745 that shorthand only at runtime; the regex catches unquoted, quoted, and
746 whitespace-separated property syntax. Replace every match with the four
747 explicit border-side widths before deployment.
74810. Run the completeness gate on the page you wrote — the same one 5c runs on a
749 worker's page, and this is also the page a 5c fallback produces:
750 
751 ```powershell
752 node "${PLUGIN_ROOT}/scripts/genpage-worker-output.js" --file '<working-dir>/<filename>.tsx'
753 ```
754 
755 On `"ok":false`, rewrite the page once from the same plan inputs and run the
756 gate again; if it still fails, halt with the reported problems rather than
757 deploy an incomplete page.
75811. Proceed to Phase 6
759 
760This saves ~5-15s of Task overhead and ~3K tokens that would otherwise be
761duplicated in a subagent context.
762 
763#### 5c. Multi-page: invoke page-builders in parallel
764 
765**If the plan's Pages table contains 2+ rows**, first stamp every target, so the gate after the
766workers can tell a page a worker wrote from one an earlier attempt left there (continue only on
767`"ok":true`). Here and below, `<filename>` and `[filename]` are each page's `File` value from the
768plan without its `.tsx` extension (`candidate-tracker.tsx` gives `candidate-tracker`):
769 
770```powershell
771node "${PLUGIN_ROOT}/scripts/genpage-worker-output.js" --stamp --file '<working-dir>/<filename>.tsx'
772```
773 
774Then invoke a `genpage-page-builder`
775agent via the `Task` tool per page. **Fire all invocations in a single message**
776for parallel execution.
777 
778For each page, pass a prompt that includes:
779 
780- Page name (e.g., "Candidate Tracker")
781- Target file name (e.g., "candidate-tracker.tsx")
782- Absolute path to `genpage-plan.md`
783- Data mode (see below) — either a RuntimeTypes path or an explicit mock flag
784- **Connectors: `none` or `<n> binding(s)`** — the Phase 4.5 binding-count outcome, verbatim
785- **Telemetry: `enabled` or `disabled`** — the Phase 4.7 probe result, verbatim
786- Working directory
787- Plugin root: `${PLUGIN_ROOT}`
788 
789**For Dataverse pages**, include the RuntimeTypes line:
790 
791> You are the genpage-page-builder agent. Generate the **[Page Name]** page.
792>
793> - Target file: [filename].tsx
794> - Plan document: [absolute path to genpage-plan.md]
795> - Data mode: **dataverse**
796> - Connectors: **[none|<n> binding(s) from Phase 4.5]**
797> - Telemetry: **[enabled|disabled from Phase 4.7]**
798> - RuntimeTypes: [absolute path to RuntimeTypes.ts]
799> - Working directory: [absolute path from Phase 0]
800> - Plugin root: ${PLUGIN_ROOT}
801>
802> Follow the instructions in your agent file. Write [filename].tsx and return your
803> result when done.
804 
805**For mock data pages**, omit the RuntimeTypes line and set `Data mode: mock`:
806 
807> You are the genpage-page-builder agent. Generate the **[Page Name]** page.
808>
809> - Target file: [filename].tsx
810> - Plan document: [absolute path to genpage-plan.md]
811> - Data mode: **mock**
812> - Connectors: **[none|<n> binding(s) from Phase 4.5]**
813> - Telemetry: **[enabled|disabled from Phase 4.7]**
814> - Working directory: [absolute path from Phase 0]
815> - Plugin root: ${PLUGIN_ROOT}
816>
817> Follow the instructions in your agent file. Write [filename].tsx and return your
818> result when done.
819 
820Wait for all page-builder tasks to complete before proceeding.
821 
822After the parallel workers return, validate every target file with the deterministic
823completeness gate:
824 
825```powershell
826node "${PLUGIN_ROOT}/scripts/genpage-worker-output.js" --file '<working-dir>/<filename>.tsx'
827```
828 
829The script reuses the source-literals checks for a complete default export (a file
830cut off inside its own export line fails), unbalanced brackets, and a file that
831stops mid-statement (inside JSX, a string or a comment, or after an operator), and also rejects a markdown code fence around the code and
832elided code — a `FIXME` comment, a `TODO` that opens a comment or takes a colon,
833a comment opening with `...`, "omitted for brevity", or a bare `...` line. The
834same words in strings or JSX text ("Loading…"), or as prose in a comment ("the
835todo list"), are UI copy and pass. It also refuses a page that is exactly as its dispatch stamp
836recorded it: the worker wrote nothing, and what is there is an earlier attempt's. If a worker reported missing declared file/process tools, produced no file,
837or `genpage-worker-output.js` returns `"ok":false`, do not re-dispatch that worker.
838Run the Phase 5b page-builder workflow inline for only the failed page, preserving
839the same plan and dispatch inputs. This is the same inline fallback path for missing
840and invalid worker output. Then Grep every generated page with
841`['"]?borderWidth['"]?\s*:` and replace the unsupported Griffel shorthand before Phase 6.
842 
843### Phase 6: Deploy
844 
845For each `.tsx` file produced, deploy to Power Apps.
846 
847If Phase 4.5 wrote `<working-dir>/connectors.json`, first pre-flight the active
848PAC CLI:
849 
850```powershell
851pac model genpage upload --help
852```
853 
854The help output must contain `--connectors`. If it does not, stop and surface:
855"connector deploy requires a pac build with `pac model genpage upload
856--connectors` — build from PowerPlatform-Scale-AdminTools or update pac." Do
857not silently drop bindings.
858 
859Connector deployment matrix:
860- **Create (new page):** include `--connectors '<working-dir>/connectors.json'`
861 with the first `upload --add-to-sitemap`.
862- **Edit — connectors changed, added, or one removed:** write the full desired
863 binding set to `connectors.json` and include `--connectors` with
864 `upload --page-id '<id>'` (full replace).
865- **Edit — no connector change:** omit `--connectors`; pac preserves existing
866 bindings. Never pass a stale or empty file on an unrelated edit.
867- **Delete all connectors:** write `[]` to `connectors.json` and pass
868 `--connectors` so pac clears the page's `connectorBindings`.
869 
870If Phase 4.6 wrote `<working-dir>/actions.json`, pre-flight the same way — the upload `--help`
871must contain `--actions`; if not, stop and surface "Custom API deploy requires a pac build with
872`pac model genpage upload --actions` (PowerPlatform-Scale-AdminTools)." Don't silently drop bindings.
873 
874Custom API deployment follows the **identical matrix** as connectors, substituting
875`--actions '<working-dir>/actions.json'` for `--connectors`: pass it on create; on an edit only
876when bindings changed/added/removed (full replace); omit it on an unrelated edit (pac preserves
877existing); write `[]` and pass it to clear all `actionBindings`.
878 
879**Deploy through `scripts/genpage-upload.js`, never by composing a raw `pac model genpage upload` command.** The script is a thin wrapper over the same upload path `/app-builder` uses: it hands the prompt and agent-message to pac **by file** (`--prompt-file`/`--agent-message-file`). A prompt is arbitrary user text — quotes, newlines, `%VAR%`, `&`, `|`, non-ASCII — and putting it on a command line means the shell gets to reinterpret it. That failed live with:
880 
881```text
882Error: Not a valid command.
883Parse failed on: Inspection
884Was it quote wrapped? No, be sure to wrap values that contain spaces.
885```
886 
887…for a prompt containing an ASCII-quoted multiword page name. **Never "fix" that by editing the approved prompt** (for example swapping in typographic quotes): the page would then be built from text the user never approved.
888 
889**Write the prompt, the agent-message and, on a create, the page's display name to files first**, then pass the
890paths. The display name is the maker's text too: substituted into a double-quoted `--name` value, PowerShell expanded `$(…)` in
891it before the script ran, and `Revenue $100` arrived as `Revenue `. Pass it with `--name-file`, never `--name`.
892 
893**Write these files with your file-writing tool (Write), never with a shell command.** No quoting makes the maker's
894text safe inside a command: a single-quoted here-string ends at a line that begins with `'@` (or `‘@`, `’@`), and the
895rest of that line runs. First check that none of the names is a link — a write through a link or hard link left at
896one of them rewrites the file it points to, outside the working directory included — and clear the files an earlier
897deploy left, so the tool writes each one fresh. This step carries no text:
898 
899```powershell
900$wd = '<working-dir>'
901foreach ($f in 'prompt.txt', 'agent-message.txt', 'page-name.txt') {
902 $at = Get-Item -LiteralPath (Join-Path $wd $f) -Force -ErrorAction SilentlyContinue
903 if ($at -and ($at.LinkType -or $at.PSIsContainer)) { throw "$f in $wd is a link or a folder, not a file this skill wrote: remove it and re-run" }
904 if ($at) { Remove-Item -LiteralPath $at.FullName -Force }
905}
906```
907 
908Then write, with the file tool, `<working-dir>/prompt.txt` holding the prompt, `<working-dir>/agent-message.txt` the
909agent message and, on a create, `<working-dir>/page-name.txt` the page's display name — each exactly its text (a
910trailing line break on the name is ignored). `genpage-upload.js` refuses any of them that is a link, a hard link or a
911folder, but only after the write. The name is the one value passed to pac inline. A name containing a straight double
912quote (`"`) is refused before anything is uploaded — pac stores each one as `\"`, in the page and in the navigation
913title it writes — so use typographic quotes (“ ”) or an apostrophe, which are stored exactly. Where pac is installed as
914a `pac.cmd` shim (Windows), a name containing `%` is refused too, so pick one without it.
915 
916**Log the invocation into `workflow-log.md` under a `## Phase 6 — Deploy` section before running it.** Record the flags and the prompt-file path, plus the prompt's scope, so the approved text is preserved semantically without embedding arbitrary text as an executable command. Format:
917 
918```markdown
919## Phase 6 — Deploy
920- Command: `node "${PLUGIN_ROOT}/scripts/genpage-upload.js" --env '<org-url>' --app-id '<id>' --code-file '<path>' --data-sources '<entities>' --prompt-file '<working-dir>/prompt.txt' --model '<model-id>' --name-file '<working-dir>/page-name.txt' --agent-message-file '<working-dir>/agent-message.txt' --add-to-sitemap`
921- Prompt scope: full page description from plan's `## User Requirements` (create) — or the delta only (update)
922- Result: page-id = <returned-id>, status = success
923```
924 
925When present, the logged command must also include `--connectors '<working-dir>/connectors.json'`
926and/or `--actions '<working-dir>/actions.json'`.
927 
928#### Prompt semantics
929 
930- **First upload** (`--add-to-sitemap`, no `--page-id`): full page description
931 from plan's `## User Requirements`.
932- **Any subsequent upload** (`--page-id`, no `--add-to-sitemap`): delta only —
933 the changes in this upload, written like a commit message, never a
934 re-statement of the original.
935 
936`--add-to-sitemap` is **refused** together with `--page-id` — an update cannot add a sitemap
937entry, and the page it names is already placed. If a create is recovered as an update after a
938mid-flight failure, the page is deployed but **not** placed, and that is reported as a failed
939(incomplete) deployment carrying the page id, not as success.
940 
941Applies in Phase 6 updates, Phase 6.5 PAGEREF re-uploads, Phase 7.5 fix
942re-deploys, and the entire edit flow.
943 
944#### For Dataverse entity pages (first upload — create):
945 
946```powershell
947node "${PLUGIN_ROOT}/scripts/genpage-upload.js" `
948 --env '<org-url>' `
949 --app-id '<app-id>' `
950 --code-file '<working-dir>/<file>.tsx' `
951 --name-file '<working-dir>/page-name.txt' `
952 --data-sources 'entity1,entity2' `
953 --connectors '<working-dir>/connectors.json' `
954 --actions '<working-dir>/actions.json' `
955 --prompt-file '<working-dir>/prompt.txt' `
956 --model '<current-model-id>' `
957 --agent-message-file '<working-dir>/agent-message.txt' `
958 --add-to-sitemap
959```
960 
961Omit the `--connectors` line when Phase 4.5 did not write `connectors.json`, and the
962`--actions` line when Phase 4.6 did not write `actions.json`.
963 
964**For mock data pages:** Same but omit `--data-sources`.
965 
966#### For updating existing pages (subsequent upload):
967 
968Use `--page-id`, omit `--add-to-sitemap`, and **scope the prompt to the delta only**:
969 
970```powershell
971node "${PLUGIN_ROOT}/scripts/genpage-upload.js" `
972 --env '<org-url>' `
973 --app-id '<app-id>' `
974 --page-id '<page-id>' `
975 --code-file '<working-dir>/<file>.tsx' `
976 --data-sources 'entity1,entity2' `
977 --connectors '<working-dir>/connectors.json' `
978 --actions '<working-dir>/actions.json' `
979 --prompt-file '<working-dir>/prompt.txt' `
980 --model '<current-model-id>' `
981 --agent-message-file '<working-dir>/agent-message.txt'
982```
983 
984For updates, include the `--connectors` line only when this upload intentionally
985replaces or clears connector bindings; otherwise omit it to preserve the
986deployed page's current bindings. The same rule applies to `--actions` for Custom
987API bindings: include it only when this upload intentionally replaces or clears them.
988 
989An update without `--name-file` keeps the page's current display name, and one without `--model` its
990current model: pac would otherwise rename the page to its navigation title and store an empty model, so
991the script reads both from the deployed page and sends them again. If either cannot be read — or the
992name cannot be sent, because pac is a `pac.cmd` shim and the name holds `%` or `"` — the update still
993goes ahead and its result carries a `warnings` entry naming what may have changed; re-run with that value
994passed explicitly.
995 
996### Phase 6.5: Navigation Fix-Up (Multi-Page Only)
997 
998Runs only when the plan has 2+ pages AND any built `.tsx` contains a `PAGEREF_`
999token. Page-builders emit `pageId: "PAGEREF_<filename-without-tsx>"` as a
1000placeholder because GUIDs don't exist until after Phase 6 (see Rule 13). This
1001phase substitutes the real GUIDs.
1002 
1003#### Steps
1004 
10051. Build `filename-without-tsx → page-id` map from Phase 6 upload output.
10062. **Sort keys by length descending** so `PAGEREF_pet` can't match inside
1007 `PAGEREF_pet-gallery`.
10083. For each `.tsx` in `<working-dir>/*.tsx` (top level only, no recursion),
1009 replace every quoted `"PAGEREF_<name>"` (must be in double quotes — that's
1010 the format page-builders emit) with `"<page-id-guid>"`.
10114. If a placeholder doesn't match any map key (typo, missing sibling), stop
1012 and report — never silently ship the literal string.
10135. Re-upload only the files that had at least one replacement. Use the update form
1014 of `scripts/genpage-upload.js` (`--page-id`, no `--add-to-sitemap`). Per the
1015 "Prompt semantics" rule in Phase 6, this is an **update**, so the prompt
1016 describes the delta only — not the original page description:
1017 
1018 Check and clear the two names as in Phase 6, then write them with the file tool: `prompt.txt` holding
1019 `Resolve cross-page navigation placeholders to real page GUIDs (post-deploy fix-up)` and `agent-message.txt`
1020 holding `Replaced PAGEREF_<name> tokens with actual page IDs returned by Phase 6`. Then:
1021 
1022 ```powershell
1023 node "${PLUGIN_ROOT}/scripts/genpage-upload.js" `
1024 --env '<org-url>' `
1025 --app-id '<app-id>' `
1026 --page-id '<page-id-from-Phase-6>' `
1027 --code-file '<working-dir>/<file>.tsx' `
1028 --data-sources 'entity1,entity2' `
1029 --prompt-file '<working-dir>/prompt.txt' `
1030 --model '<current-model-id>' `
1031 --agent-message-file '<working-dir>/agent-message.txt'
1032 ```
1033 
1034Pages with no `PAGEREF_` strings need no second upload.
1035 
1036### Phase 6.7: Solution Packaging (ALM, optional)
1037 
1038Runs only when the plan's `## Solution Packaging` has `Package into solution: true`.
1039Adds the deployed app, the GenPage(s), and any connection references to the
1040target solution so they travel cross-environment.
1041 
10421. Ensure the solution exists — create it only if it doesn't already exist:
1043 `node "${PLUGIN_ROOT}/scripts/provision-solution.js" '<envUrl>' '<solutionUniqueName>' '<Friendly Name>' [--publisher '<uniqueName>']`
1044 It prints `{ "ok": true, "solutionId": …, "uniqueName": …, "publisherPrefix": … }`;
1045 `uniqueName` must start with a letter and contain only letters, digits, and
1046 underscores. Without `--publisher` it resolves the environment's default publisher.
10472. Add the app + GenPage(s) + connection references (pass the page-id(s) returned
1048 by Phase 6 as `--page-ids` — the GenPage is added explicitly, it does NOT travel
1049 with the app on its own):
1050 `node "${PLUGIN_ROOT}/scripts/add-page-to-solution.js" '<envUrl>' '<solutionUniqueName>' '<app-id>' --page-ids '<page-id1,page-id2>' --connection-refs '<logicalName1,logicalName2>'`
10513. Log the command + result to `workflow-log.md`.
1052 
1053Cross-env note: the app (80) pulls the sitemap (62); the GenPage
1054`uxagentproject` is added explicitly and pulls its `uxagentprojectfile` rows
1055(including `config.json` with `connectorBindings`); each `connectionreference`
1056is added so bindings resolve. The script discovers both custom-table component
1057types from `EntityDefinitions(...).ObjectTypeCode` in the target environment —
1058their numeric values are environment-specific and must never be hardcoded. At import the
1059deployer supplies env-specific `ConnectionId` per connection reference via
1060`pac solution create-settings` + `pac solution import --settings-file`.
1061 
1062Custom API bindings need **no** extra ALM step: `config.json`'s `actionBindings` travels
1063automatically in the `uxagentprojectfile` rows already pulled with the GenPage. The
1064referenced Custom APIs are a separate deployment prerequisite (bound by `name`), not added here.
1065 
1066### Phase 7: Verify in Browser (Optional)
1067 
1068After successful deployment, ask the user via `AskUserQuestion`:
1069> "Would you like to verify the page(s) in the browser using Playwright?"
1070 
1071Options: **Yes, verify in browser** / **Skip verification**
1072 
1073- If the user picks **Skip verification** → jump to Phase 8.
1074- If the user picks **Yes** → read `${PLUGIN_ROOT}/skills/genpage/verify-flow.md`
1075 for the full Playwright verification workflow (navigate, structural
1076 verification including below-the-fold, interactive testing, screenshots,
1077 fix-and-redeploy). The orchestrator only loads that file on demand to keep
1078 context lean when verification is skipped.
1079 
1080### Phase 8: Summary
1081 
1082By Phase 8 the `workflow-log.md` should already contain Phase 0 through Phase 7
1083sections written incrementally — the planner writes Phase 1 inside its agent
1084context, you (the orchestrator) write Phase 0 / 0.5 / 3 / 4 / 6 / 6.5 / 7 as
1085each runs, and the entity-builder and page-builder agents append their own
1086Phase 2 / 5 sections when invoked.
1087 
1088In Phase 8, append a final `## Phase 8 — Summary` section to the same file:
1089 
1090```markdown
1091## Phase 8 — Summary
1092 
1093| Page | File | Entities | Status |
1094|------|------|----------|--------|
1095| <Name> | <file>.tsx | <entities or "mock data"> | Deployed |
1096 
1097- App: <name> (<app-id>)
1098- Entities created: <list, or "none">
1099- Browser verification: <skipped | confirmed | failed: <reason>>
1100```
1101 
1102The log MUST contain command-level entries for every prereq / auth / question /
1103upload / script invocation — not just outcome summaries. The eval harness greps
1104the log for tokens like `node --version`, `pac auth list`, `AskUserQuestion`,
1105`EnterPlanMode`, `--prompt`, `check-auth.js`, etc. A decision-only log
1106(e.g., `Decision: new page` without the underlying `AskUserQuestion`) will
1107fail Layer 1 assertions even when the agent's behavior was correct.
1108 
1109Then present a final summary to the user:
1110 
1111```
1112## Genpage Complete
1113 
1114| Page | File | Entities | Status |
1115|------|------|----------|--------|
1116| [Name] | [file].tsx | [entities or "mock data"] | Deployed |
1117 
1118App: [app name] ([app-id])
1119Screenshots: [if verification was done]
1120Next steps: Share with team, iterate on design, create additional pages
1121```
1122 
1123 
1124---
1125 
1126## Edit Flow
1127 
1128For the edit flow (triggered when the `genpage-planner` returns
1129`{ "action": "edit" }`), see [edit-flow.md](edit-flow.md) in this folder.
1130 
1131The edit flow has its own 8 phases (Edit Phase 1-8): discover and select target
1132app + page via `pac model list` + `pac model genpage list`, download, generate
1133RuntimeTypes if needed, invoke `genpage-edit-planner`, apply the edit inline,
1134deploy, verify, summarize.
1135 

Discussion