How-to: solution
Solution lifecycle recipes, taken from the CRMWorx build (§1, §5). See the CLI reference for every flag.
Create a publisher, then the solution (zero web-UI prerequisite)
crm --json solution create-publisher --name crmworx --display CRMWorx --prefix cwx \
--option-value-prefix 30000 --if-exists skip
crm --json solution create --name CRMWorx --publisher crmworx --if-exists skip
solution create-publisher auto-wires publisher_prefix=cwx back onto the active named profile. --if-exists skip makes re-runs a no-op. Every customization-write command still needs its own --solution <name> — creating the solution here does not set a default; see crm apply / crm metadata create-entity / etc.
List the components in a solution
crm --json solution components CRMWorx
--json row carries componenttype (raw integer), componenttypename (friendly string, e.g. "entity", "webresource", "sla"), objectid, rootcomponentbehavior, and additional OData fields. Use it to verify the model landed.
Common component-type codes (full map in crm/core/solution_components.py):
| code | name |
|---|---|
| 1 | entity |
| 2 | attribute |
| 3 | relationship |
| 9 | optionset |
| 10 | entityrelationship |
| 14 | entitykey |
| 20 | role |
| 24 | form |
| 26 | savedquery |
| 29 | workflow |
| 36 | emailtemplate |
| 44 | duplicaterule |
| 59 | savedqueryvisualization |
| 60 | systemform |
| 61 | webresource |
| 62 | sitemap |
| 63 | connectionrole |
| 70 | fieldsecurityprofile |
| 90 | plugintype |
| 91 | pluginassembly |
| 92 | sdkmessageprocessingstep |
| 95 | serviceendpoint |
| 150 | routingrule |
| 151 | routingruleitem |
| 152 | sla |
| 153 | slaitem |
| 154 | convertrule |
| 155 | convertruleitem |
Unmapped codes fall back to the integer as a string in componenttypename. The componenttype/objectid/rootcomponentbehavior triple is the tuple key used by --save/--diff below.
Detect drift: save & diff a component inventory
# Capture the expected inventory once (normalized bare JSON list)
crm --json solution components CRMWorx --save components.json
# Later: compare live components against the saved snapshot
crm --json solution components CRMWorx --diff components.json
--save writes a normalized JSON list to <path> (parent dirs created as needed) and emits {"saved": "<path>", "count": N}. Each entry carries exactly three keys: {"componenttype": <int>, "objectid": "<guid-lowercase>", "rootcomponentbehavior": <int|null>}.
--diff fetches live components and compares them against the file, keying each component on the tuple (componenttype, objectid, rootcomponentbehavior). The data field contains {"matches": bool, "missing": [...], "unexpected": [...]} — missing = in expected but not live, unexpected = in live but not expected. Exits non-zero (1) on drift so agents and CI can branch on $?; exit 0 means the live solution matches the snapshot exactly.
The two flags are mutually exclusive; bare components <name> is unchanged. The round-trip --save then --diff against the same org reports no drift (#82).
Check what an exported solution needs before importing
Run this read-only check against the import target org before importing — an empty result means the org already has everything the solution requires.
crm --json solution missing-components ./MySolution.zip
SOLUTION_FILE is a path to an exported solution .zip (not a solution unique
name — the API requires the file bytes). The command calls
RetrieveMissingComponents against the connected org and returns the list of
components the solution depends on that are absent there.
--json emits {ok, data:[<missing components>], meta:{count}}. A non-empty
data list names what must be installed (or the import will fail with a
dependency error). Human mode lists the components and prints the count.
Gotcha — URL-length limit. The file bytes are sent as a parameter in the
query string. Very large solution zips can exceed the server's URL-length limit;
that is an inherent constraint of the RetrieveMissingComponents function, not a
CLI bug. Pre-validate offline with solution validate first; if the zip is
excessively large, split it across multiple solutions.
Detect unmanaged-layer conflicts across two solutions
crm --json solution layer-conflicts --solution MyManagedSln --unmanaged-solution MyDevSln
Reports components present in both a managed and an unmanaged solution — i.e. managed components that also carry unmanaged-layer customizations, the potential unmanaged-layer conflicts. The result is the intersection of the two solutions' solutioncomponents, keyed on (componenttype, objectid) and deliberately ignoring rootcomponentbehavior (the same component included with a different behavior is still an overlap). Each row carries componenttype, the friendly type_name (or the raw int as a string for an unmapped type), objectid, and both sides' managed_rootcomponentbehavior / unmanaged_rootcomponentbehavior.
Works identically on v9.x on-prem and Dataverse online — it needs only solutioncomponents (present since CRM 2011), not the online-only msdyn_componentlayer, so on-prem gets a detection path it otherwise lacks. Read-only: --solution must resolve to a managed solution and --unmanaged-solution to an unmanaged one (validated client-side; a wrong-kind flag fails with {ok:false} and exit 1 naming the offending flag). Always exits 0 when both kinds are valid — conflicts found or not (reporting, not failure, unlike components --diff); zero conflicts emits an explicit "no conflicts found" message and an empty list with meta.count = 0.
Limitation: matching is at solution-component granularity. A table added whole to the managed solution whose single attribute was customized and added to the unmanaged solution intersects on nothing — the attribute is its own component with its own objectid/type. Subcomponent-level correlation is out of scope (#200).
Add or remove a component
# add an existing web resource (componenttype 61) to an unmanaged solution
crm --json solution add-component --solution CRMWorx --type webresource --id <guid>
# remove it again (destructive — prompts unless --yes)
crm --json solution remove-component --solution CRMWorx --type 61 --id <guid> --yes
AddSolutionComponent / RemoveSolutionComponent actions. --type takes a componenttype integer or a friendly name (entity, attribute, relationship, optionset, webresource, …; names are case- and separator-insensitive — WebResource, web resource, web-resource all resolve to 61). Pass a raw integer for any type not in the name map. Both refuse a managed solution client-side (a managed solution can't be edited). Note the canonical split: relationship is 3 (base relationship) and entityrelationship is 10 — not interchangeable.
add-component is non-destructive. AddRequiredComponents defaults on (--no-add-required turns it off) and subcomponents are included by default (--no-subcomponents sets DoNotIncludeSubcomponents: true). Adding an entity with AddRequiredComponents on emits an informational meta.note: the server may silently add required components beyond the requested entity, and the response does not report them (#181).
remove-component is destructive: it prompts for confirmation unless --yes; in a non-TTY context it fails fast (exit 1) with an error that names --yes — the standard ok:false envelope under --json, a human-formatted error otherwise. The agent-side PreToolUse hook also blocks it without --yes (#71).
Preview what blocks uninstalling a managed solution
crm --json solution dependencies CRMWorx
RetrieveDependenciesForUninstall and returns the components that would block uninstalling that managed solution: {solution, blockers[], count}, each blocker carrying dependent_type, dependent_id, dependent_parent_id, required_type, dependency_type (the same shape as metadata dependencies). Human mode prints a blocker table; an empty result means nothing blocks the uninstall. Read-only — the GET fires even under --dry-run. This is the solution-scoped counterpart to metadata dependencies: that command targets a single component (entity/attribute/optionset/relationship); this one takes only a solution unique name. An unknown solution name returns a clean {ok:false} envelope (#116).
Bump the version (or friendly name / description) before export
crm --json solution set-version CRMWorx --version 1.0.1.0
crm --json solution set-version CRMWorx --friendly-name "CRM Worx" --description "RC build"
--dry-run previews the PATCH). --version must be 4-part dotted numeric and is validated before any HTTP; at least one field is required. Managed solutions and patches are rejected client-side (the server returns CannotUpdateSolutionPatch for a patch) (#66).
Export the unmanaged solution to a zip
crm solution export CRMWorx -o docs/artifacts/crmworx.zip
managed: False, and the action that ran (falls back to synchronous ExportSolution when ExportSolutionAsync is disabled on-prem). On success the zip is written to -o/--output; adding --json only changes the printed result envelope.
Omit the solution name on an interactive terminal and crm solution export -o <path> lists the org's solutions (unmanaged first) and lets you pick one with the arrow keys. Under --json or with no TTY (scripts, agents, CI) the name stays a required argument — a missing one is a usage error (exit 2), unchanged.
Source-control a solution (extract / pack)
# Unpack an exported zip into a diff-able folder tree
crm solution extract --zipfile docs/artifacts/crmworx.zip --folder src/CRMWorx
# ...commit the tree, review `git diff`, then build a zip back from it
crm solution pack --zipfile dist/crmworx.zip --folder src/CRMWorx
extract / pack are thin wrappers over the Power Platform CLI pac solution unpack / pac solution pack: extract unpacks an exported solution zip into a folder of XML/source files you can commit, and pack rebuilds an importable zip from that folder. There is no XML-diff engine — git diff on the extracted tree is the solution diff.
These are offline local-file transforms: they never open a connection, and no profile or credentials are required. --package-type selects Unmanaged (default), Managed, or Both. The executable is resolved in order: --pac-path → the CRM_PAC environment variable → PATH (the pre-migration --solutionpackager-path flag and CRM_SOLUTIONPACKAGER env var remain as deprecated aliases — point them at pac, not SolutionPackager.exe). crm does not bundle or download pac — install the cross-platform Power Platform CLI (dotnet tool install --global Microsoft.PowerApps.CLI.Tool); an absent binary fails with an error naming it. pac runs on Linux, macOS, and Windows, replacing the legacy, Windows-only SolutionPackager.exe that Microsoft no longer recommends.
--timeout bounds the subprocess (seconds). The result envelope carries {action, exit_code, folder, zipfile, stdout_tail} (only the tail of pac's chatty output is kept); a non-zero exit_code fails the command (ok: false, exit 1) while still reporting stdout_tail for diagnosis (#73).
Validate a solution package before import
Catch packaging problems offline in one pass instead of one-error-per-import round-trip:
crm solution validate ./MySolution.zip
Offline checks: every component in customizations.xml is declared in
solution.xml <RootComponents> and vice-versa; $webresource: references in
ribbon XML resolve to a web resource in the package; every global option-set
binding is declared; both manifests are well-formed and all required members
(solution.xml, customizations.xml, [Content_Types].xml) are present.
Add --against-org to also check the connected org for colliding formid /
savedqueryid GUIDs, colliding BPF process-stage GUIDs (StageId /
NextStageId read from Workflows/*.xaml and probed against processstages —
the CreateProcessStage duplicate-key import failure), the existence of
referenced web resources and global option sets, and whether the package's
SolutionPackageVersion exceeds the target org version — a package newer than
the org (even a newer minor) fails import with 0x80048068 ("you can only import
solutions with a package version of {org} or earlier"). The version check is
best-effort: an absent/unparseable package version or an org version that can't
be read degrades to a warning/skip and never falsely flips the report invalid.
Requires a connection/profile:
crm solution validate ./MySolution.zip --against-org
validate exits non-zero when any error-severity problem is found, so it drops
straight into a pre-import CI gate.
Import a solution zip
crm solution import docs/artifacts/crmworx.zip --yes
PublishWorkflows, not PublishAllXml), so it is gated as a destructive operation: without --yes it prompts for confirmation and, in a non-TTY context, fails fast (exit 1) with an error naming --yes — the standard ok:false envelope under --json, a human-formatted error otherwise. Always pass --yes when invoking non-interactively (agents, CI). Use --no-overwrite to keep existing unmanaged customizations, or --no-publish to suppress workflow activation — both are the off-halves of boolean pairs (--overwrite/--no-overwrite, --publish/--no-publish); the positive spellings are also accepted. The --no-overwrite path skips the in-band overwrite prompt, but the destructive-op gate still requires --yes for any import since it mutates the org (#67).
On completion the result parses the import job's data column into a solution-level result (success/warning/failure) plus a components list — {name, type, result, errorcode?, errortext?} per imported component. A component that failed under an overall-succeeded job is no longer hidden: any non-success component adds a meta.warnings note, so status: succeeded can't mask a partial failure (#70). Add --formatted to also attach the Excel-format RetrieveFormattedImportJobResults report verbatim under formatted_results (opt-in — it is a separate round-trip).
The result also includes a managed field: true if the imported solution is managed, false if unmanaged, or null when the flag could not be read (e.g. a corrupt zip). This is sniffed from solution.xml inside the zip before the upload and is present in dry-run results too (#91).
On on-prem orgs that reject ImportJobId on ImportSolutionAsync (v9.x), the command transparently retries with the synchronous ImportSolution action carrying the same id (action: "ImportSolution" in the result), so import_job_id is always non-null and import-result works there too (#182). On that path the whole import runs inside one HTTP request — the read timeout follows --timeout (default: the profile's async_timeout), and no progress ticks are emitted. A missing-dependency import fails loudly as a synchronous error (naming the import_job_id) instead of reporting a bare status: succeeded; if the platform still provides no per-component results after a successful import, meta.warnings says so explicitly.
If an import is blocked by a product-update dependency (the server rejects it before processing components), add --skip-dependency-check to set SkipProductUpdateDependencies: true in the request body and allow the import to proceed past that check (#376). This flag applies to both the async and the synchronous-fallback path.
Verify a prior import
crm --json solution import-result <import_job_id>
meta.warnings on any non-success component) without re-importing. The <import_job_id> is the import_job_id reported by solution import. Add --formatted for the Excel-format report (#70).
Upgrade a managed solution (clone-as-patch, stage-and-upgrade, apply-upgrade, uninstall)
First-class verbs for the managed-solution lifecycle, so an agent never has to drop to the raw CloneAsPatch/DeleteAndPromote server actions. They all work on both v9.x on-prem and Dataverse online, and compose with --dry-run and --json.
Clone a parent solution to a patch (CloneAsPatch):
crm --json solution clone-as-patch --solution CRMWorx
{cloned, parent_solution, display_name, version, patch_solutionid}. When --version is omitted the parent's version is read and its revision (4th part) is bumped — e.g. a parent at 1.0.0.0 yields patch 1.0.0.1; a patch must keep the parent's major.minor. --display defaults to the parent's friendly name. A missing parent fails before any POST.
Stage an upgrade as a holding solution (ImportSolution HoldingSolution):
crm solution stage-and-upgrade docs/artifacts/crmworx_2_0.zip --yes
solution import (per-component result parsing, the on-prem synchronous ImportSolution fallback, progress ticks). Because it shares that pipeline it also accepts the same two escape hatches as solution import: --skip-dependency-check (set SkipProductUpdateDependencies: true to proceed past a product-update dependency block) and --formatted (also attach the Excel-format RetrieveFormattedImportJobResults report verbatim under formatted_results). Gated as destructive: --yes skips the prompt for non-interactive use.
Apply the staged upgrade with --promote (DeleteAndPromote — replaces the base solution and its patches with the holding solution):
crm solution stage-and-upgrade docs/artifacts/crmworx_2_0.zip --promote --solution CRMWorx --yes
--promote requires --solution (the unique name to promote, exit 2 otherwise). It runs only after a real, succeeded stage — never under --dry-run — and attaches the promote result under data.promote.
Apply a separately-staged upgrade (DeleteAndPromote — the standalone promote path):
crm solution apply-upgrade CRMWorx --yes
stage-and-upgrade (run without --promote), decoupling stage-time from promote-time — the same DeleteAndPromote the one-shot stage-and-upgrade --promote runs, replacing the base solution and deleting its patches. Gated as destructive (--yes).
Uninstall a solution (DELETE /solutions(<id>)):
crm solution uninstall --solution CRMWorx --yes
RetrieveDependenciesForUninstall and refuses with the blocker count unless --force (use solution dependencies to inspect the blockers first). For a managed base solution the server also uninstalls its patches. Gated as destructive (--yes).
Project a solution into a desired-state spec (org-to-org drift)
Generate an apply-consumable YAML spec from every component in a solution — entity, attribute, global option set, view, 1:N relationship, main form, security role, web resource, and model-driven app — in one pass. This is the source side of the org-to-org drift recipe: run export-spec on dev, then apply --dry-run on prod to preview schema drift without writing anything.
# Dev org: project the solution into a spec file
crm solution export-spec MyCustomSolution -o desired.yaml
# Prod org: preview what drifts — pure dry-run, no writes
crm --dry-run apply -f desired.yaml
Without -o, a summary data payload plus a skipped bucket is emitted under the standard JSON envelope.
crm --json solution export-spec MyCustomSolution
Security roles (component type 20) project under security_roles — name, optional business unit, and privileges grouped by depth into privilege_names selector rows. Roles whose privileges are all at non-authorable depths (e.g. RecordFilter) are routed to skipped.
Web resources (component type 61) project under webresources — the body is carried inline as base64 content (no sidecar file needed), plus display_name and webresourcetype. The emitted spec round-trips through apply directly.
Forms ride along inside each touched entity's projection (ADR 0024): the entity's seedable main form is emitted under its forms: block — the custom-field placement, script libraries, and event handlers a real apply can layer back onto a fresh org's platform main form. Non-seedable form content is dropped to warnings (a field whose control type has no seedable classid), and a non-seedable whole form (an additional, non-primary main form) is reported in skipped.
Model-driven apps (component type 80) project under a top-level apps: block (ADR 0024) — a separate pass over the solution's appmodule members, since an app is not entity-rooted. Each app emits its identity (name, unique_name, optional description) and its Entity-backed sitemap (areas → groups → subareas), the seedable slice: a subarea's table logical name is portable, so re-applying the spec on a fresh org reproduces the navigation. Non-seedable content is surfaced, never dropped silently — a Url / dashboard subarea (bound to an org-specific target) is dropped to warnings, and record-backed component bindings (views / forms / charts / BPFs, bound by org-specific id) are reported in warnings with a count (tables reach the app through the sitemap's entity subareas). An app whose unique_name lacks a publisher prefix (a first-party app apply cannot re-create) is routed to skipped. A standalone sitemap member (component type 62) is reported in skipped — it is projected under its app, not on its own.
Components that cannot be projected — plug-in assemblies, dashboards, workflows, additional main forms, and other non-seedable types — appear in a skipped bucket {type, objectid, reason}. The verb never fails on an unsupported component (exit 0, ok: true) and never drops one silently.
Known limitation: projection is driven by entity members only — the exporter does not resolve a subcomponent member to its parent entity. So attribute, view, and relationship members always appear in skipped; their data is still exported when the parent entity is itself in the solution (projected in full), and only a lone subcomponent whose parent entity is absent is genuinely not exported (ADR 0019).
The emitted spec includes a top-level solution: key so apply --dry-run auto-scopes its drift/prune report. The entity-level counterpart is metadata export-spec, which projects a single entity; solution export-spec composes it across every entity in the solution.
Publish all customizations
crm --json solution publish-all
PublishAllXml so newly created metadata, views, forms, charts and dashboards surface in the app. Dataverse serializes customization/publish operations org-wide behind a single lock, so a concurrent operation (another dev's UI publish, a second live run) can fail with a CustomizationLockException; the CLI auto-retries the lock-error family with bounded exponential backoff on every customization write — not just publish-all — and surfaces it only if the lock never frees (see the retry note in the README).