Skip to content

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
Create both from the CLI before any metadata work (#34); 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
Returns one row per component. Each --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
Wrap the 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
Calls 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"
Updates an unmanaged solution in place over the shared record-update path (so --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
Reports the output path, byte count, 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
By default an import overwrites unmanaged customizations in the target org and activates imported workflows (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>
Re-fetches a completed import job by id and runs the same parser, returning the per-component pass/fail envelope (and the same 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
Creates a patch solution and returns {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
Imports the zip in holding mode — staged for upgrade, not yet applied — reusing the same import pipeline as 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
Promotes a solution already staged via 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
Resolves the solutionid, then pre-checks 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
Calls 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).