On-demand deployment of the dispatcher PCF for a third-party .ppmplugin control to a Power Platform environment. The dispatcher PCF is the Studio-side control that dispatches the composite key <name>/<receiver> over the wrap shell's SendMessagePlugin bridge to the control's native module. Runs pac pcf push against the user's active pac auth profile. Decoupled from /generate-pcf-companion — the engineer scaffolds locally, customizes / iterates, then deploys when ready.
Read shared/shared-instructions.md, shared/naming-conventions.md.
Apply the per-skill minimal prereq policy (shared-instructions.md §1.5). This skill needs Node.js 20+ with npm, pac CLI, .NET SDK, and an active pac auth profile. It does not need pnpm.
Print the prereq status as a visible block per shared-instructions.md §9.2 before continuing:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Prereq check — /publish-pcf-companion
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🟢 ✓ Node.js 20+ and npm installed (for npm install / npm run build)
🟢 ✓ pac CLI installed (for pac pcf push)
🟢 ✓ .NET SDK installed (solution build runs inside pac pcf push)
🟢 ✓ pac auth profile active (target env: <env URL from pac org who>)
🟢 4 checks passed. Ready to proceed.
Fix table for failures:
| Missing | → Fix: line in the failure block |
|---|---|
| Node.js / npm | Install Node.js 20 LTS from https://nodejs.org, then verify with node -v and npm -v |
pac CLI |
dotnet tool install -g Microsoft.PowerApps.CLI.Tool (chains on .NET SDK first if also missing) |
| .NET SDK | brew install dotnet (mac) / winget install Microsoft.DotNet.SDK.10 (win) / package manager (linux) |
No active pac auth |
pac auth create --environment <your-env-url> (interactive browser flow; use an identity with access to the target Power Platform environment). Do not reach for --deviceCode first — it's a headless-shell fallback that commonly fails under Conditional Access. |
Run the /publish-pcf-companion check from prereq-check.md. Per the auto-fix policy (shared-instructions §1.5): a missing pac CLI is auto-fixable when .NET is present — offer dotnet tool install -g Microsoft.PowerApps.CLI.Tool and continue on yes; no active pac auth → initiate pac auth create --environment <url> (browser) and verify after. If an auth attempt fails, walk the variant ladder in prereq-check.md (browser → device code only if headless → back to browser with --environment → pac auth clear) — change a variable each step and never re-run a variant that already failed. A missing .NET SDK hard-stops (system-wide install — print the command).
Read ./PRD.md. Required — the skill derives the PCF folder name (pcf/<Pascal>PCF/) and publisher prefix from PRD §2. If PRD is missing, STOP with BLOCKED: PRD.md missing — cannot determine PCF folder name.
Read ./.extension-state.md if present. The state file is informational; this skill works without it but uses Phase info to surface "scaffold-pcf hasn't happened yet" early.
Build a status dashboard so the user sees what was detected before any action.
Discover the PCF folder robustly — don't assume the exact nesting depth. Use Glob to find
pcf/**/ControlManifest.Input.xml and select the first match. pac pcf init normally produces
pcf/<Pascal>PCF/<Pascal>PCF/ControlManifest.Input.xml, but case differences and manual
restructuring should not break discovery. If pcf/ is absent, stop with
BLOCKED: no pcf/ directory — PCF has not been scaffolded. Run /generate-pcf-companion first. If
pcf/ exists but the manifest is absent, list the relevant files with Glob and stop with
BLOCKED: pcf/ exists but no ControlManifest.Input.xml was found.
Derive the PCF project root from the manifest path by moving up two directory levels. Use Glob to
check whether <PCF_PROJECT_ROOT>/out/ exists. Use Read to extract the <control version="X.Y.Z">
value from the manifest and the latest deployment version from .extension-state.md. Run
pac org who to retrieve the active environment because that is a real toolchain command.
Print:
PCF deploy status
─────────────────
Repo: <cwd>
PCF project root: <PCF_PROJECT_ROOT from find>
Manifest: <MANIFEST path>
Built (out/): <yes | no — will build now>
Current manifest version: <CURRENT_VERSION>
Last deployed version: <LAST_DEPLOYED or "none — first deploy">
Publisher prefix: <LAST_PREFIX from .extension-state.md, else `pamext` default — confirmed in Step 3.0>
Active pac auth env: <env URL from `pac org who`>
Active pac auth user: <user from `pac org who`>
Plan: pick publisher prefix → bump version if chosen → build if needed → pac pcf push --publisher-prefix <chosen prefix>
Why the version matters: Power Platform caches PCF controls by version in deployed apps. If you re-push the same version, apps that already loaded the previous bundle may not see your changes until their cache invalidates (timing varies — sometimes minutes, sometimes hours, sometimes never until the maker re-publishes the app). Best practice is to bump the patch version on every meaningful push.
If pac org who returns "No active connection": STOP — this means pac auth list showed a profile but it's not currently selected. Run pac auth select --index <n> and re-run this skill. (Step 1 catches missing auth; this catches the rarer "auth exists but not active" case.)
Why Glob instead of an exact path: the standard layout is nested two levels below the project root, but case differences, manual restructuring, and non-standard scaffolders occur. Glob provides OS-neutral discovery without assuming a fixed path.
This step has THREE gates: publisher prefix → version-bump → deploy confirmation. Each is its own AskUserQuestion.
The publisher prefix becomes part of the solution name in Power Platform (e.g. pamext_<Pascal>PCF). It must be 2–8 characters — pac pcf push rejects anything outside that range with Argument --publisher-prefix has incorrect length.
Use Read on .extension-state.md and extract the most recent Publisher prefix: <value> entry. If
the file or entry is absent, use pamext as the default.
Then ask via AskUserQuestion:
Publisher prefix for this deployment? (must be 2–8 lowercase chars)
- <LAST_PREFIX> (use the prefix from the previous deployment) — only shown if
LAST_PREFIXwas found- pamext (recommended) — 6 chars; "PAM Extension"; the default we suggest for first-party native-extension PCFs
- mspa — 4 chars; "Microsoft PowerApps"
- Custom — supply your own 2–8-char prefix (free-text input, validate length before continuing)
Validate the chosen value:
PREFIX="<from user choice>"
PREFIX_LEN=${#PREFIX}
if [ "$PREFIX_LEN" -lt 2 ] || [ "$PREFIX_LEN" -gt 8 ]; then
echo "❌ Publisher prefix '$PREFIX' is $PREFIX_LEN chars; must be 2–8. Try again."
# Re-prompt
fi
if ! [[ "$PREFIX" =~ ^[a-z][a-z0-9]*$ ]]; then
echo "❌ Publisher prefix must start with a lowercase letter and contain only lowercase letters and digits."
# Re-prompt
fi
Why the prefix matters: it groups your PCF with other solutions under the same publisher identity in Power Platform's solution explorer. Use the same prefix across all first-party native-extension PCFs so they appear together. The recommended pamext is a project convention — your team may have its own; ask before picking custom on a shared environment.
Why we don't hardcode it: the team / environment may have an existing publisher prefix established. Hardcoding "powerapps" was wrong on two counts — it's 9 chars (over the limit) AND it doesn't respect environment conventions.
Use AskUserQuestion. Compute the bump-target options from CURRENT_VERSION (the version currently in the manifest):
Current PCF version: <CURRENT_VERSION> (last deployed: <LAST_DEPLOYED or "never">)
Power Platform caches PCFs by version. Re-pushing the same version may not invalidate caches in apps that already use this control. Bump the version?
- Patch bump → <CURRENT major.minor.(patch+1)> (recommended for bug fixes, internal improvements) — typical default
- Minor bump → <CURRENT major.(minor+1).0> (for new features or new outputs/configurable inputs the maker can opt into)
- Major bump → <(CURRENT major+1).0.0> (for breaking changes to the bound input, output names, or trigger semantics)
- No bump — push as-is at <CURRENT_VERSION> (only if you're iterating during initial dev and accept the cache risk; the skill will print a warning)
Apply the chosen bump with the Edit tool by replacing only the <control version="..."> attribute
in the manifest. Show the exact version change (<CURRENT_VERSION> → <NEW_VERSION>) before writing
and preserve every other manifest field.
After bumping, re-build is required (the manifest changed, so out/ is stale). The build runs as part of Step 4 regardless of the user's choice in 3.2 below, so this is fine.
If user picked "No bump": skip the manifest edit. Print a warning:
⚠️ Pushing at <CURRENT_VERSION> again. Apps that previously loaded this control may not see your changes until their PCF cache invalidates. Use a version bump for the next push if this matters.
Wrong-env deploys are a confirmed failure mode (a publish once went to wrap-bug-bash-env instead of wrap-player-test-env because the env URL was buried in a long question). So make the target environment the headline of this gate, and explicitly offer to change it — never silently reuse whatever pac auth happens to be active. Show the FULL current-env details (not just the URL) so the user can tell which env it is: read pac org who (and pac auth list for the friendly profile name) and print:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
►► CURRENT DEPLOY TARGET ◄◄
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Environment: <friendly org name from `pac org who` — e.g. "Contoso Test (default)">
URL: <env-url>
Environment ID:<org/environment id from `pac org who`, if shown>
Signed-in as: <user from `pac org who`>
Auth profile: <active profile name from `pac auth list` (the ★ row)>
Last deployed: <from .extension-state.md PCF deployments — env + timestamp, or "first deploy">
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Then a third AskUserQuestion whose question text names the env again, verbatim, and leads with the keep-or-change decision:
Deploy
<Pascal>PCF@<resolved version>(prefix<PREFIX>) to— . Keep this environment, or switch?
- Yes, keep this env — build + push (recommended): runs
npm run build, thenpac pcf push --publisher-prefix <PREFIX>.- Switch environment — change the target before deploying. Run
pac auth listand offer the existing profiles (pac auth select --index <n>), or create a new one (pac auth create --environment <url>), in-flow — then re-print this headline and re-ask. Don't make the user leave and re-run the skill.- Push without rebuild (only if you didn't bump version AND you trust the current
out/): skipsnpm run build. Disabled if 3.1 bumped the version — a bumped manifest requires a rebuild.- Cancel
The env (friendly name + URL) and prefix appear twice, verbatim (the headline block + the question) — typos in env config or the wrong active auth are the most common causes of "the wrong tenant got the deploy." Always surface the current env and the switch option even when an auth profile already exists — never assume the active profile is the one the user wants.
Use the $PCF_PROJECT_ROOT discovered in Step 2 — don't hardcode pcf/<Pascal>PCF:
cd "$PCF_PROJECT_ROOT"
[ -d node_modules ] || npm install --no-audit --no-fund
npm run build --silent
If build fails: STOP with BLOCKED: pcf-scripts build failed — fix the source and re-run /publish-pcf-companion. Direct user to /test-native-extension Layer 4 for diagnostics.
cd "$PCF_PROJECT_ROOT"
pac pcf push --publisher-prefix "$PREFIX" # $PREFIX from Step 3.0
The prefix $PREFIX was chosen by the user in Step 3.0 and length-validated (2–8 chars). If pac pcf push still rejects with Argument --publisher-prefix has incorrect length, something downstream (env config, prefix regex change in newer pac versions) is unexpected — surface the raw error and re-run Step 3.0 to pick a different value.
If the prefix doesn't exist as a publisher in the target environment, the push errors with Publisher prefix '$PREFIX' not found. Surface this and direct the user to either:
pac solution publisher-add --prefix "$PREFIX" --name "<publisher name>", ORPrint:
🟢 ✓ Deployed <Pascal>PCF to <env-url> at <ISO timestamp>
Solution: <PREFIX>_<Pascal>PCF (verify in https://make.powerapps.com → Solutions)
pac pcf push can fail for several reasons. Common ones:
| Error | Likely fix |
|---|---|
Publisher prefix '<PREFIX>' not found |
Env admin needs to add the prefix, or re-run Step 3.0 and pick one that exists in the env |
Authentication failed / 401 |
pac auth token expired or the profile is bound to the wrong identity/env. Try pac auth select, then re-create with pac auth create --environment <url> (browser). If that fails, walk the variant ladder in prereq-check.md — do not retry the same variant, and do not default to --deviceCode. |
Solution import failed: missing dependency |
Env is missing a required Power Platform feature (rare for native-extension PCFs; surface the dep name) |
Build of <Pascal>PCF.csproj failed |
The PCF tooling tried dotnet build and it failed. Often a stale out/ — run rm -rf out/ && npm run build then retry. |
Update .extension-state.md: under a new "PCF deployments" section, add Deploy: fail — <ISO> — <env-url> — <one-line reason>. STOP with BLOCKED: pac pcf push failed — <reason>.
After a successful push, update ./.extension-state.md:
## PCF deployments
- <ISO timestamp> — <env-url> — Version: <X.Y.Z> — Publisher prefix: <PREFIX> — Result: success
Append a new line on each subsequent successful deploy. Most recent at the bottom. The Version: field is the value from <control version="..."/> at push time (post-bump if 3.1 bumped it). The Publisher prefix: field is what the user chose in Step 3.0 — subsequent runs default to this value via the LAST_PREFIX detection.Print final summary:
Deploy complete
───────────────
PCF: <Pascal>PCF
Version: <X.Y.Z> ← post-bump if 3.1 bumped; else same as before
Bump: <patch | minor | major | none — re-pushed at same version>
Environment: <env-url>
Publisher prefix: <PREFIX>
Build: <skipped — current out/ used | rebuilt at <time>>
Deployed at: <ISO timestamp>
Where to look next (informational — these are makers-portal steps, not skills):
• Open https://make.powerapps.com → switch to <env> → Solutions → find `<PREFIX>_<Pascal>PCF`.
• In a Canvas app: Insert → Custom → search for `<Pascal>PCF` under the `PowerApps` namespace.
• If a previously-deployed app doesn't show your changes: re-publish that app (File → Save → Publish) to bust the PCF cache.
Per shared/shared-instructions.md §9.1: surface real next-step choices via AskUserQuestion with context-aware options based on repo state. Execute, don't describe — when the user picks a Run /… option, immediately invoke that skill via the Skill tool in the same turn (shared-instructions §9.1). The only option that ends the run without invoking anything is the escape hatch.
Detect state to filter options:
| Detector | Implies |
|---|---|
No .ppmplugin bundle built yet (ppmplugin/<name>.ppmplugin absent) |
/generate-ppmplugin is the natural next step — the dispatcher PCF is deployed but the binary bundle it dispatches to still needs to be built. |
A .ppmplugin bundle already exists (ppmplugin/<name>.ppmplugin present) |
/audit-ppmplugin re-verifies the built bundle; /debug-extension debugs/refines the control. |
Then AskUserQuestion. Typical post-deploy options:
Question: "Dispatcher PCF deployed. What would you like to do next?"
Header: "Next step"
Options:
1. "Run /generate-ppmplugin" description: "Build (or rebuild) the verified .ppmplugin binary bundle end-to-end (manifest → build → assemble → audit) — the bundle this dispatcher PCF routes to over the wrap bridge."
2. "Run /debug-extension" description: "Debug/refine the control (PRD or native/PCF code), then re-deploy with another version bump."
3. "Run /audit-ppmplugin" description: "Re-verify an already-built .ppmplugin bundle against the format spec and the manifest's receiver/method contract."
4. "Stay — I'll verify in Studio" description: "Skill exits. Open make.powerapps.com → your env → find the new PCF in Solutions / insert it in a Canvas app to verify."
Informational (not a skill option): uploading the built
.ppmpluginvia the wrap wizard and wiring this dispatcher PCF into a canvas app is Stage 3 — not yet a skill. Once the bundle is built and the PCF is deployed, that final hosting step is a manual makers-portal / wrap-wizard flow.
When the user picks a Run /… option, invoke it via the Skill tool. When they pick "Stay", print one line confirming the deploy is done and proceed to the return-status block.
| Code | Meaning |
|---|---|
DONE |
pac pcf push succeeded; state file updated with deployment row. |
DONE_WITH_CONCERNS: <list> |
Push succeeded but with caveats (publisher prefix mismatch and user accepted the alternate; or npm run build produced warnings the user accepted). |
NEEDS_CONTEXT: <missing> |
PRD missing, PCF not yet scaffolded, or required user input incomplete in a scenario question. |
BLOCKED: <reason> |
Prereq failed (pac, dotnet, or auth missing), pac pcf push rejected for a non-collision reason (perms, network), npm run build failed, user cancelled, or pac org who shows no active connection. |
After the first line, blank line, then the human-readable summary.
yes to that URL. If env changes between Step 3 and Step 4 (e.g. pac auth select was run by a parallel session), STOP and re-prompt.--force-style flags to bypass auth or solution conflicts. If pac pcf push errors, surface and stop — don't try alternative prefixes or solution targets without user confirmation..extension-state.md if present, otherwise to pamext. Never push without an explicit prefix confirmation — environments often have established prefixes and the wrong one creates an orphan solution.pac auth profile; if the user wants a different env, they switch it BEFORE running the skill.<control version="X.Y.Z"> in the manifest) drives Power Platform's cache invalidation. The skill MUST present the bump options at Step 3.1 with AskUserQuestion, default-highlighting patch bump, and apply the chosen bump verbatim. Never bump silently and never skip the prompt — the engineer needs to see "we're changing your manifest version" before it happens.out/ with a new manifest version is a real bug (the deployed bundle wouldn't match the version declared in the solution).