export-solution

Exports a Dataverse solution containing Power Pages site components as a zip file, ready for deployment to another environment. Use when asked to: "export solution", "download solution", "export managed", "export unmanaged", "package for deployment", "create solution zip", "export site package", or "build deployment artifact".

Provider: Microsoft Power Platform APIs Path in repo: plugins/power-pages/skills/export-solution/SKILL.md

Skill body

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

export-solution

Triggers an async Dataverse solution export, polls until complete, downloads the solution zip, and verifies it. Reads .solution-manifest.json to identify the solution; falls back to asking the user.

Prerequisites

Phases

Phase 0 — ALM plan gate

plan-alm is the front door. When the user expresses an ALM intent (promote / ship / deploy / set up CI-CD / move to staging / push to prod), the orchestrator (/power-pages:plan-alm) should run first. This Phase 0 enforces that and is meant to fail closed when there’s no plan, not to be a one-time check the user can dismiss forever.

Skip rule. If this skill was invoked as part of an active plan-alm orchestration, skip Phase 0 entirely and proceed to Phase 1. The gate helper exposes this via its inExecution block — pass through silently to Phase 1 when:

inExecution.status === "active"

The helper computes this from docs/.alm-plan-data.jsonPLAN_STATUS === "In Execution" AND LAST_INVOCATION_AT within the last 60 minutes. check-alm-plan.js refreshes LAST_INVOCATION_AT automatically on every invocation that finds the plan in execution, so each in-chain skill keeps the chain alive for the next one — even multi-hour deploys (deploy-pipeline alone can take 60 min per stage) survive the window without the chain incorrectly de-classifying. Stalled chains (no heartbeat for > 60 min) reclassify as stale-heartbeat and Phase 0 gates fire normally so an abandoned plan doesn’t silently bypass user confirmation.

When inExecution.status is anything other than "active" ("not-running", "stale-heartbeat", "no-plan"), run the Phase 0 gate flow below. Branch on the remaining helper fields:

Step 1 — Run the gate helper.

node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/check-alm-plan.js" \
  --projectRoot "." \
  --envUrl "{envUrl from .solution-manifest.json or pac env who, if available}" \
  --token "{token, if Phase 1 already acquired one}" \
  --solutionId "{solutionId from .solution-manifest.json, if available}"

The helper returns JSON with { exists, deferred, stale, staleness: { reason, detail }, generatedAt, planStatus, ... }. The freshness check requires env credentials + solutionId; without those the helper does an existence-only check.

Step 2 — Branch on the result.

Result Behavior
deferred: true The user has explicitly deferred ALM for this project (.alm-deferred marker present). Pass through silently to Phase 1 — do not nag.
exists: false The user hasn’t run plan-alm yet. See Step 3.
exists: true, stale: false Plan is current. Pass through silently to Phase 1.
exists: true, stale: true (reason: solution-modified) The solution changed after the plan was generated. See Step 4.

Step 3 — No plan. Tell the user:

“No ALM plan exists for this project. /power-pages:plan-alm builds one — it detects the project state, asks about your promotion strategy (PP Pipelines vs Manual export/import), and orchestrates the right skills (including this one) in the right order. Want me to run plan-alm now?”

🚦 Gate (intent · export-solution:0.no-plan): Fail-closed entry gate when check-alm-plan.js returns exists:false. Helper-script-backed.

AskUserQuestion:

Question Header Options
Run /power-pages:plan-alm first? ALM plan gate Yes — run /power-pages:plan-alm now (Recommended), Continue without a plan (advanced — I know what I’m doing), Cancel

Step 4 — Stale plan. Tell the user:

“ALM plan exists from {generatedAt} but the source solution has been modified since (at {solution.modifiedon}). Components may have changed. Re-running plan-alm will refresh the analysis and the rendered HTML.”

🚦 Gate (intent · export-solution:0.stale-plan): Fail-closed entry gate when check-alm-plan.js returns stale:true. Helper-script-backed.

AskUserQuestion:

Question Header Options
Refresh the plan first? ALM plan freshness Refresh — re-run /power-pages:plan-alm (Recommended), Continue with the existing plan, Cancel

Why this gate exists. Direct invocation of export-solution produces a zip without the orchestrator’s pre-export completeness check. Users running this skill standalone often miss components that should have been added to the solution (cloud flows, env var values referenced by site settings, sample data references) and ship a zip that imports cleanly into staging but produces a broken site post-deploy. The pre-plan completeness check surfaces those gaps before any zip is built. The gate ensures plan-alm either ran (so completeness was verified and the export was scoped to the right solution lineage) or the user explicitly chose to bypass it.

Phase 1 — Verify Prerequisites

Create all tasks upfront at the start of this phase.

Tasks to create:

  1. “Verify prerequisites”
  2. “Identify solution”
  3. “Configure export”
  4. “Trigger async export”
  5. “Download solution zip”
  6. “Verify export”
  7. “Present summary”

Steps:

  1. Run verify-alm-prerequisites.js with --require-manifest to confirm PAC CLI auth, acquire a token, verify API access, and validate that .solution-manifest.json exists:
    node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/verify-alm-prerequisites.js" --require-manifest
    

    Capture output as JSON; extract .envUrl (store as envUrl) and .token (store as token). If the script exits non-zero, stop and explain what is missing (reference ${CLAUDE_PLUGIN_ROOT}/references/dataverse-prerequisites.md).

Phase 1.5 — Ground in current ALM documentation

Reference: ${CLAUDE_PLUGIN_ROOT}/references/alm-docs-grounding.md

Cap this step at ~30 seconds. If MCP search / fetch errors out, log a one-line note and continue — this skill must remain runnable offline.

  1. Run microsoft_docs_search with the query: Power Pages solution export managed unmanaged ExportSolutionAsync ALM.
  2. Fetch https://learn.microsoft.com/en-us/power-platform/alm/solution-concepts-alm (and at most one sister page on managed vs unmanaged or solution layering) in parallel via microsoft_docs_fetch.
  3. Extract a one-paragraph summary of what Microsoft Learn currently says about export semantics, managed vs unmanaged implications, and async export polling. Compare against ${CLAUDE_PLUGIN_ROOT}/references/solution-api-patterns.md and flag any divergence in ExportSolutionAsync / DownloadSolutionExportData signatures.
  4. Use the summary to inform Phase 2+ decisions. Do not silently change skill behavior — surface any divergence to the user as a soft warning before Phase 3.

Phase 2 — Identify Solution

  1. Look for .solution-manifest.json in project root (use findProjectRoot or glob('**/.solution-manifest.json'))
  2. If found: read solution.uniqueName, solution.solutionId, environmentUrl
    • Verify environment URLs match (warn if different — may be cross-environment export)
  3. If not found: ask user for solution unique name via AskUserQuestion
  4. Confirm solution exists in environment:
    GET {envUrl}/api/data/v9.2/solutions?$filter=uniquename eq '{solutionName}'&$select=solutionid,uniquename,friendlyname,version,ismanaged
    
  5. Present solution details and confirm with user.

Phase 2.5 — Pre-export Completeness Check

Before exporting, run the shared site-inventory helper to detect any components that exist on the site but are not in the solution. Catching this here avoids shipping an incomplete package to staging/prod.

node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/discover-site-components.js" \
  --envUrl "{envUrl}" --token "{token}" \
  --siteId "{websiteRecordId}" \
  --publisherPrefix "{publisherPrefix from .solution-manifest.json}" \
  --solutionId "{solutionId}"

Parse stdout and evaluate missing. Before doing anything else, capture the pre-sync state so a post-sync re-confirmation gate can show what changed:

PRE_SYNC_VERSION = solutionManifest.solution.version   // from .solution-manifest.json read in Phase 2
PRE_SYNC_MISSING = { siteComponents, siteLanguages, cloudFlows, envVarDefinitions, customTables, ... }   // from the discovery stdout above

Then:

Why the post-sync gate exists: when sync mode runs mid-export, it produces a different solution version than the one the user had in mind when they invoked the skill. Re-confirming after sync gives the user an explicit chance to inspect the version bump and the list of newly-adopted components before the zip is produced and (typically) shipped onward via import-solution. The Phase 2.5 trigger is intentional; the post-sync re-confirmation is the safety on top of it. This mirrors the same gate in deploy-pipeline Phase 3.5 — same shape, same options, same audit-trail rules — so users see consistent behavior whether they take the PP Pipelines path or the Manual export/import path.

Why Phase 2.5 exists in the first place: historically, components created after setup-solution (server logic from add-server-logic, flows from add-cloud-flow, env vars from configure-env-variables / setup-auth) were silently left out of the export zip and didn’t travel to target environments. The ALM-aware-by-default principle in AGENTS.md requires this check at every gate where a solution leaves its source environment.

Phase 3 — Configure Export

🚦 Gate (consent · export-solution:3.export-type): Managed vs Unmanaged — irreversible for the produced zip. Managed cannot be edited in target; Unmanaged can. Mismatch with stage strategy ships the wrong artifact downstream.

Invoke AskUserQuestion immediately — do NOT describe this choice as chat text. The user must answer live before export proceeds.

Question Header Options
How would you like to export this solution? Managed solutions cannot be edited in the target environment and support clean upgrade/delete cycles — recommended for staging and production. Unmanaged solutions can be edited in the target environment — use for dev-to-dev deployments. Export Type Managed — for staging/production (Recommended), Unmanaged — for development environments

Use the answer to set "Managed": true or "Managed": false in the ExportSolutionAsync request body.

Also ask (separate AskUserQuestion):

Phase 4 — Trigger Async Export

Step 4.0 — Bump source solution version (always-on).

Before exporting, bump the patch segment (4th segment) of the source solution’s version. Without this, two consecutive exports without intervening setup-solution sync produce zips that carry the same version string — and importing the second zip into a target that already has the first installed is unreliable for managed solutions (no clean upgrade path) and depends on OverwriteUnmanagedCustomizations: true for unmanaged.

Why always-on, not “only when sync mode added components”: setup-solution only bumps when it has new components to add. A user who modifies content of an already-in-solution component (a web template, a site setting value, a web file) and then re-exports must still get a strictly-increasing version label — otherwise the manual export/import path quietly ships stale-version zips. See the Why this step exists callout in setup-solution Phase 4.

node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/bump-solution-version.js" \
  --envUrl "{envUrl}" \
  --token "{token}" \
  --uniqueName "{solutionUniqueName}" \
  --projectRoot "."

Capture output as JSON; store .previous as PRE_EXPORT_VERSION, .next as EXPORT_VERSION, and inspect .manifestUpdated / .manifestUpdateReason to confirm the manifest sync succeeded. Report: “Bumped solution {solutionUniqueName} from v{PRE_EXPORT_VERSION} to v{EXPORT_VERSION} for export.”

--projectRoot "." makes the helper update .solution-manifest.json’s solution.version (single-solution) or matching solutions[].version (multi-solution) field atomically as part of the bump operation — no separate Edit step needed. If the manifest doesn’t exist or has no matching entry, manifestUpdated: false and manifestUpdateReason tells you why (no-manifest, no-matching-entry, etc.); the bump itself still succeeded.

If the bump already happened earlier in this session (e.g. setup-solution sync mode ran with adopted components in Phase 2.5 and bumped the version, then handed back here): the helper still runs and bumps again. This is intentional — sync’s bump is paired with new components; export’s bump is paired with the produced zip. They’re independent concerns and double-bumping is cheap (just an extra patch segment). The skill-skipping logic for “the manifest version already matches the live source version” is intentionally NOT added here; it would create a class of “I edited content but no sync was needed and no bump happened, so the export shipped a stale version” failures.

Step 4.1 — Trigger async export.

Run scripts/lib/export-solution-async.js to POST ExportSolutionAsync, poll until terminal state, and return the AsyncOperationId:

node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/export-solution-async.js" \
  --envUrl "{envUrl}" \
  --token "{token}" \
  --solutionName "{solutionUniqueName}" \
  --managed {true|false}

Capture stdout as JSON; extract .asyncOperationId (store as asyncOperationId).

Report: “Export job started. Polling for completion…”

Handle script exit code:

Phase 5 — Download Solution Zip

Run scripts/lib/download-export-data.js to POST DownloadSolutionExportData, decode the base64 zip, and write it to disk:

node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/download-export-data.js" \
  --envUrl "{envUrl}" \
  --token "{token}" \
  --asyncOperationId "{asyncOperationId}" \
  --outputPath "{outputDir}/{SolutionUniqueName}_{managed|unmanaged}.zip"

Capture stdout as JSON; extract .zipPath (store as zipPath) and .fileSizeBytes.

Report: “Downloading solution zip…”

Handle script exit code:

Phase 6 — Verify Export

  1. Confirm zip file exists on disk: check fs.existsSync(zipPath)
  2. Confirm file size > 1000 bytes
  3. Verify Solution.xml is inside the zip:
    • Run unzip -l "{zipPath}" | grep -i solution.xml or read zip TOC via Node.js (use Bash with unzip)
    • If solution.xml not found: report error — the zip may be corrupt or the download was truncated

Phase 7 — Present Summary

Step 7.1 — Write docs/alm/last-export.json marker.

Ensure docs/alm/ exists, then write the marker so downstream skills (import-solution skew advisory, refresh-alm-plan-data.js rendering the Manual-path tab, future “modified-since-last-export” gates, audit trail) can reason about what was last shipped from this source.

node -e "require('fs').mkdirSync('docs/alm',{recursive:true})"

Then write docs/alm/last-export.json:

{
  "exportedAt": "<ISO timestamp>",
  "solutionUniqueName": "<solutionUniqueName>",
  "solutionId": "<solutionId from .solution-manifest.json or Phase 2 query>",
  "previousVersion": "<PRE_EXPORT_VERSION from Step 4.0>",
  "version": "<EXPORT_VERSION from Step 4.0>",
  "managed": <true|false>,
  "sourceEnvironmentUrl": "<envUrl>",
  "zipPath": "<zipPath>",
  "fileSizeBytes": <fileSizeBytes>,
  "asyncOperationId": "<asyncOperationId>"
}

The path is registered in scripts/lib/alm-paths.js under the key lastExport — programmatic consumers should resolve via almPath(projectRoot, 'lastExport') rather than re-inlining the path string. (Skill prose inlines the path verbatim for readability, matching the convention used for last-deploy.json, last-import.json, and the other ALM markers.)

Step 7.2 — Display the summary.

Item Value
Solution {solutionUniqueName} v{EXPORT_VERSION} (was v{PRE_EXPORT_VERSION})
Export type Managed / Unmanaged
File {zipPath}
File size {size} KB
Export job {AsyncJobId}
Marker written docs/alm/last-export.json

Suggested next steps:

Record Skill Usage

Reference: ${CLAUDE_PLUGIN_ROOT}/references/skill-tracking-reference.md

Follow the skill tracking instructions in the reference to record this skill’s usage. Use --skillName "ExportSolution".

Refresh the ALM plan (if one exists)

node "${CLAUDE_PLUGIN_ROOT}/scripts/lib/refresh-alm-plan-data.js" \
  --projectRoot "." \
  --phase export-solution \
  --render

Re-renders docs/alm-plan.html so any step-status updates the agent made during this skill (Export solutionstatus-completed) flow through. When docs/.alm-plan-data.json is absent (standalone invocation, not via plan-alm), the helper returns ok:false as a soft no-op — safe to run unconditionally.

Key Decision Points (Wait for User)

  1. Phase 2: Solution identification — confirm before triggering export
  2. Phase 2.5: Completeness-gap prompt (sync-first / export-as-is / abort) when the live site has components missing from the solution
  3. Phase 2.5: Post-sync approval gate — only fires after a mid-export sync (Option 1). Shows the new solution version + newly-adopted components and asks the user to confirm the post-sync solution before exporting the zip. Pause exits cleanly; Cancel aborts.
  4. Phase 3: Managed vs unmanaged — affects downstream importability (irreversible choice for this export)
  5. Phase 4 Step 4.0: No user prompt — version bump runs automatically before ExportSolutionAsync. The bumped version (PRE_EXPORT_VERSION → EXPORT_VERSION) is surfaced in the Phase 7 summary so the user can see what version landed in the zip.

Error Handling

Progress Tracking Table

Task subject activeForm Description
Verify prerequisites Verifying prerequisites Confirm PAC CLI auth, acquire Azure CLI token, verify API access
Identify solution Identifying solution Read .solution-manifest.json or ask user, confirm solution exists in environment
Configure export Configuring export Ask user: managed vs unmanaged, output directory
Trigger async export Triggering async export Bump source solution version (Step 4.0) via bump-solution-version.js so the zip carries a strictly-increasing version label; POST ExportSolutionAsync, capture AsyncJobId, poll until complete
Download solution zip Downloading solution zip POST DownloadSolutionExportData, decode base64, write zip to disk
Verify export Verifying export Confirm zip exists, size > 0, Solution.xml present inside
Present summary Presenting summary Write docs/alm/last-export.json marker (via alm-paths.js); show zip path, size, type, version bump, and suggested next steps

Skill frontmatter

user-invocable: true argument-hint: Optional: 'managed' or 'unmanaged' (default: asks) allowed-tools: Read, Write, Edit, Bash, Glob, Grep, TaskCreate, TaskUpdate, TaskList, AskUserQuestion, mcp__plugin_power-pages_microsoft-learn__microsoft_docs_search, mcp__plugin_power-pages_microsoft-learn__microsoft_docs_fetch model: opus