Solutions
xrmd imports managed and unmanaged solutions, and exports unmanaged ones, in the native solution.zip format (solution.xml, customizations.xml,
[Content_Types].xml, assemblies and web resources), with the same messages as Dataverse: via SDK/SOAP (ServiceClient,
pac solution import/export, which use ServiceClient) and as Web API actions. Acceptance criterion B
(import of the sample solution with ServiceClient, imported plug-ins that fire, N:N, asynchronous import, export
round-trip) is the SolutionAcceptanceTests test; the manual tests with pac and XrmToolBox are in
docs/superpowers/acceptance/pac-solution-checklist.md.
Managed solutions (layers, upgrades, uninstall) are described in Managed solutions and layers
below; what is checked only against a real Dataverse is in
docs/superpowers/acceptance/managed-solutions-checklist.md.
Microsoft first-party solutions are not supported. Solutions that Microsoft publishes (for example the msdyn_*
Dynamics 365 and Power Platform ones) depend on components xrmverse does not have. An import whose publisher has the
customization prefix msdyn, msdynce, msdynmkt, mspp or adx, or a unique name that starts with microsoft or is
dynamics365 (case-insensitive), is not blocked: it logs a warning, and the same text is in the import job:
The solution '<solution>' comes from a Microsoft publisher ('<publisher>', prefix '<prefix>'). Microsoft first-party solutions are not supported by xrmverse.
Messages
| Message | Web API | Privilege | Notes |
|---|---|---|---|
ImportSolution |
POST ImportSolution (204) |
prvImportCustomization |
synchronous, one transaction, importjob row; a managed file installs, updates or (with HoldingSolution) stages |
ImportSolutionAsync |
POST ImportSolutionAsync |
prvImportCustomization |
queues an asyncoperation (operationtype 203); response AsyncOperationId, ImportJobKey (= importjobid) |
ExecuteAsync |
— | prvImportCustomization |
only with an ImportSolutionRequest, StageAndUpgradeRequest or DeleteAndPromoteRequest inside; response AsyncJobId |
StageAndUpgrade |
POST StageAndUpgrade |
prvImportCustomization |
stage and promote in one transaction (a plain install when the solution is not installed); the parameters of ImportSolution except HoldingSolution, SchemaUpdatesOnly, StageAndUpgrade and the template parameters (see Request options) |
StageAndUpgradeAsync |
POST StageAndUpgradeAsync |
prvImportCustomization |
queues an asyncoperation (operationtype 211); response AsyncOperationId, ImportJobKey |
DeleteAndPromote |
POST DeleteAndPromote |
prvImportCustomization |
parameter UniqueName; response SolutionId (the id of the former holding row) |
DeleteAndPromoteAsync |
POST DeleteAndPromoteAsync |
prvImportCustomization |
queues an asyncoperation (operationtype 207); response AsyncOperationId |
UninstallSolutionAsync |
POST UninstallSolutionAsync |
Delete on solution |
parameter SolutionUniqueName; queues an asyncoperation (operationtype 208); response AsyncOperationId |
ExportSolution |
POST ExportSolution |
prvExportCustomization |
unmanaged solutions only (see Known limits); response ExportSolutionFile |
RetrieveFormattedImportJobResults |
GET RetrieveFormattedImportJobResults(ImportJobId=@p1)?@p1=… |
Read on importjob |
FormattedResults: Excel workbook (SpreadsheetML 2003) |
AddSolutionComponent, RemoveSolutionComponent |
POST with the same name |
prvWriteSolution |
response id (solutioncomponentid) |
PublishXml, PublishAllXml |
POST with the same name (204) |
prvPublishCustomization |
no-op: metadata take effect from the commit; they reload the cache |
Delete of solution |
DELETE solutions(id) |
Delete on solution |
an unmanaged row: container only; a managed row: uninstall (see DeleteSolution below) |
Metadata: CreateEntity, CreateAttribute, CreateOneToMany, CreateManyToMany, CreateOptionSet, UpdateEntity,
UpdateAttribute, UpdateOptionSet, InsertOptionValue, DeleteEntity, DeleteAttribute, DeleteRelationship,
DeleteOptionSet (privileges prvCreateEntity, prvCreateAttribute, prvCreateRelationship, prvCreateOptionSet,
prvWriteEntity, prvWriteAttribute, prvWriteOptionSet, prvDeleteEntity, prvDeleteAttribute,
prvDeleteRelationship, prvDeleteOptionSet) and the reads RetrieveRelationship, RetrieveOptionSet,
RetrieveAllOptionSets, RetrieveAllEntities (no privilege, as for RetrieveEntity). Every write is in its own
transaction (DDL included) and the cache is published only after the commit. The SolutionUniqueName parameter (SDK) and
the MSCRM.SolutionUniqueName header (Web API) also add the component to that solution; every created component
ends up in Default anyway. N:N relationships create an intersect table that is readable in queries (link-entity too) and
written only by Associate/Disassociate (SDK) or $ref (Web API). The prv… names of the metadata privileges and
prvPublishCustomization are not verified on Learn (they follow the prv<Verb><Component> pattern). The metadata
write messages, ImportSolution, ImportSolutionAsync, ExecuteAsync and ExportSolution from a plug-in's service are
rejected (400), and so are the managed solution messages of the table above; RetrieveFormattedImportJobResults and
metadata reads are allowed.
Rules for metadata writes (SDK and Web API), which reject rather than silently diverge:
UpdateEntityandUpdateAttribute(alsoPUT EntityDefinitions(…)andPUT …/Attributes(…)) apply only a closed list of properties: the labels (DisplayName,DisplayCollectionName,Description) and, for attributes, alsoRequiredLevel,MaxLength(string, memo, growth only) and the options of picklist, multi-select,statecodeandstatuscode(new options, labels of existing ones), plusIsAuditEnabledof tables and columns when it can change (see Auditing), andChangeTrackingEnabledof tables (one-way, see below). The request is compared with the current definition: any other property with a different value is a 400 naming it (for exampleOwnershipType,MaxValue,FormatName). A body obtained with retrieve (orGET) and sent back with only the labels changed passes; options equal to the current ones are ignored (so a picklist with a global option set can be renamed too). A property with the value a new SDK object has (AttributeTypeName, an option'sIsHidden, …) counts as not given. The child collections of an entity (attributes, relationships, privileges, keys) are not part ofUpdateEntityand are ignored, as in Dataverse. On a system component, whoever is not a System Administrator changes only the labels (403 for the rest).AutoCreateAccessTeamsof a table is modelled (see Access teams):CreateEntity,UpdateEntityand a solution import (<AutoCreateAccessTeams>incustomizations.xml) set it, and the export writes the stored value. It is allowed only on user- and team-owned tables, at most 100 tables, and cannot be turned off while a template for the table exists (all 400). An import whose file has no such element leaves the table as it is. An organization that kept the element as opaque XML from an earlier version is converted once at startup (AccessTeamSettingsUpgrade); a kept1is applied only where the rules allow it. Team templates (teamtemplaterows) are not carried by a solution: the shape of a template insidecustomizations.xmlis not known, so an export holds the flag but not the templates, and an import creates none. Create them again with the Web API,xrmd access-team template createor the web admin page.ChangeTrackingEnabledof a table is modelled (see Change tracking):CreateEntity,UpdateEntityand a solution import (<ChangeTrackingEnabled>incustomizations.xml) set it, and the export writes it together with the computedCanChangeTrackingBeEnabled. It is one-way: an import of1switches a table on (400 for a table that cannot be tracked); an import of0for a table that is on never switches it off, the table is kept and the import logs a warning. An import whose file has no such element leaves the table as it is. An organization that kept the element as opaque XML from an earlier version is converted once at startup (ChangeTrackingSettingsUpgrade).IsActivity,ActivityTypeMask,HasNotesandHasActivitiesof a table are modelled (see Activities and notes):CreateEntitywithIsActivitytrue creates a custom activity under its rules, andHasNotes/HasActivitiesare one-way, set byCreateEntity,UpdateEntityand a solution import. Solutions carry them as follows:- Import. A table with
<IsActivity>1</IsActivity>is created as a custom activity, itssubjecttaken as the primary column even when the file lists it as a system column. Its common columns, party lists and EntityName columns, and the relationships of the activity model (regarding, notes, parties), are derived by xrmverse and never imported from the file, so no "System attribute … is not created" warning is written for them. When the file's version of one of them differs from xrmverse's (a label, a description, a required level, an audit or secured flag, a relationship marked custom or named differently), the import log gets one warning per difference and the import goes on; a file with labels in several languages can give many such lines.HasRelatedNotesandHasRelatedActivitiessetHasNotesandHasActivities: a1switches the table on (the A5 rules apply, 400), a0on a table that is on is kept on with a warning, and a0forHasRelatedNoteson a new custom activity is turned into true with a warning. A team- or organization-owned activity is a 400, and so is a re-import that turns an existing table into an activity or back, or changes itsActivityTypeMask. A table whose entity set name is one of the refused activity message names (Rollup,Book,RouteTo, …, see What is refused) is a 400, as it is at creation. - Export.
IsActivity,ActivityTypeMask(empty for a table that is not an activity) andHasRelatedNotes/HasRelatedActivitiesare written from the metadata,Truewhen on;Falseis written only where the imported file had the element, or for a table created in xrmverse. The export of a custom activity carries only its own columns. The type names of party list and EntityName columns arepartylistandentityname. - Uninstall. Deleting a custom activity removes its pointer and party rows. An uninstall that would delete a table
whose records have notes, or activities regarding them, is refused with 400 (dry run included); notes are not deleted
with it. Restoring a lower layer never changes
HasNotesorHasActivities(they are one-way, asChangeTrackingEnabled). - Activity, party and note rows are data, never solution components. An organization that kept
HasRelatedNotes/HasRelatedActivities/ActivityTypeMaskas opaque XML from an earlier version is converted once at startup (ActivitySettingsUpgrade); a kept1is applied only where the rules allow it, and the others are logged. - The real shape of a custom activity in a Dataverse export (its system columns, the
Typeof its party list columns, its relationships) has not been checked: see the activities checklist.
- Import. A table with
IsDuplicateDetectionEnabledof a table is modelled (see Duplicate detection):CreateEntity,UpdateEntityand a solution import (<IsDuplicateCheckSupported>incustomizations.xml) set it, and the export writes the stored value (the element used to be written as a literal0). Setting it on a table that cannot have it (an intersect table or a system table other thanaccountandcontact) is a 400 through the metadata messages; a1in an import for such a table is not applied and the import logs a warning. An import whose file has no such element leaves the table as it is. An organization that kept the element as opaque XML from an earlier version is converted once at startup (DuplicateDetectionSettingsUpgrade). Duplicate detection rules are not carried by a solution: Dataverse has them as solution components, but their shape incustomizations.xmlis not known to xrmverse. A<DuplicateRules>node is kept byte for byte and written back on export, the import logs a warning (and so does the import job), and no rule is created;AddSolutionComponentwith the rule component type keeps the usual "Component type … is not supported" 400. Create the rules again with the Web API,xrmd duplicate-detection rule createor the web admin page.CreateEntity(SDK andPOST EntityDefinitions) rejects with 400 anOwnershipTypeother than user and organization (the same rule as the import) and every table setting that xrmverse does not model (EntityColor, …) with a value different from the one the export writes for a table created in xrmverse (false/0, …). LikewiseCreateAttribute, the attributes ofCreateEntity(the SDK primary attribute, the Web APIAttributes) and the lookup ofCreateOneToManyreject with 400 the column flags that xrmverse does not model (IsSortableEnabled,IsSearchable,ImeMode, …) with a value different from the one the export writes.IsAuditEnabledis modelled: creation accepts either value, except a fixed column's (see Auditing).- Reads (
RetrieveEntity,RetrieveAllEntities,RetrieveAttribute,GET EntityDefinitionsand itsAttributes) show the unmodeled table and column settings with the value the export writes: the one preserved from the file for an imported component (see Preserved XML), the default one for a component created in xrmverse (an element with no property in the SDK is not shown;HasNotesandHasActivitiesare modelled and show the stored value, false when unset). This way a read and the export never show different values. The settings cannot be modified withUpdateEntity/UpdateAttribute(400), but a definition read and sent back with the same values passes. A preserved value that cannot be shown on its property is a 500 naming it. - The attributes of an intersect table (N:N) belong to the relationship:
CreateAttribute,UpdateAttributeandDeleteAttributeon it are 400. A lookup (CreateAttribute, or an attribute ofCreateEntity) must point to an existing entity (or to the entity being created) that is not an intersect, likeCreateOneToMany: 400 otherwise. DeleteEntitydeletes with the table its views (savedquery) and forms (systemform), as Dataverse does, and removes them from every solution. TheDeleteof a step also deletes its images (and removes them from the solutions).- 412
DuplicateRecordonly for a component that already exists (entity, entity set, attribute, relationship, global option set); any other internal error remains one.
System solutions: Default (FD140AAF-4DF4-11DD-BD17-0019B9312238), Active (FD140AAE-4DF4-11DD-BD17-0019B9312238)
and the publisher DefaultPublisher<org> (D21AAB71-79E7-11DD-8874-00188B01E34F, prefix new) are seeded with the
well-known Dataverse ids. Default registers an entity with rootcomponentbehavior 0 (subcomponents included) without rows for its
system attributes and relationships: a query on solutioncomponent of Default returns fewer rows than in
Dataverse.
Managed solutions and layers
A managed solution is a file whose solution.xml has <Managed>1</Managed>. Importing one does not copy its components
into the organization as if they had always been there: each component remembers which solution delivered which state, and
the solution can later be updated, replaced by a newer version or removed again.
The rules in short
- Layers. Every component a managed solution carries gets a Managed layer of that solution
(
meta.component_layer, one row per component and solution, holding the component as the file delivered it). A seeded component (a system table, say) that a managed solution changes first gets a System layer: its base state, captured before the first change. The state you read through the API is always the top layer's. - Order. Each managed solution has a rank (
meta.solution_rank); the layer order comes from it. A newly installed solution is above every earlier one, a holding solution is directly above its base, and the System layer is at the bottom. The top layer wins for every component type. Dataverse merges the layers of forms, site maps and app modules; xrmverse cannot, so a managed layer written under a higher layer of such a component is hidden, and the import log says so with a warning naming the component. - The Active layer. An unmanaged customization of a component that has a managed or System layer (a relabelled column,
an edited view, a row changed by
Update) puts an Active marker on top of it. Active sits above every managed layer and is what keeps your change when a managed solution is updated or upgraded. It disappears with the component, or when an import setsOverwriteUnmanagedCustomizations(below). A write that happens to set the managed values back keeps the marker. - A component created unmanaged that a managed file later contains keeps its Active marker on top; after the managed solution is uninstalled the component is deleted with it, marker included.
Install, update, stage, upgrade, promote
- Install: a managed file whose unique name is not installed. A name that is installed unmanaged is a 400, and so is an unmanaged file whose name is installed managed. A version lower than the installed one is a 400.
- Update: the same name, no flag, same or higher version. The solution's layers are replaced with the file's. Components the old version had and the new one lacks stay with their old layer (this is what Dataverse does for an update, and why an upgrade exists). A layer that is not on top is stored but does not change the component, and the log names it ("kept: layer above").
- Stage (
HoldingSolutiontrue): the file must have a strictly higher version. It creates the solution{name}_Upgrade, a managed row in the same publisher, whose layers sit directly above the base's. The base is unchanged until the promote. While a holding solution exists, another import of the same name (an update) is a 400. - Promote (
DeleteAndPromote): removes the base's layers, deletes the components that no layer holds any more (behind the data guard below), renames the holding row to{name}and keeps its id, which is the response'sSolutionId. It is refused with0x8004F01Dwhen a component the base created (its lowest layer is the base's, no System layer) is not in the upgrade but another managed solution has a layer on it; the message names the component and that solution. - StageAndUpgrade: stage and promote in one transaction. For a name that is not installed it is a plain install.
- Same-version StageAndUpgrade is refused (the stage needs a higher version).
OverwriteUnmanagedCustomizationsnow has an effect on managed imports: before the file is applied, the Active markers of its components are removed, so the file's state shows, except where another managed solution sits above.- Concurrent operations. An import decides what it does (install, update, stage) before its transaction; under the
metadata lock it reads the solution rows again (the solution and its
{name}_Upgrade, locked to the commit) and classifies the file again. If another import, upgrade or uninstall changed them meanwhile, the import is a 409 ("changed while it was being imported"; retry) and nothing changes. A promote and an uninstall plan again under the lock in the same way: a plan that would delete more than the first one, or (promote) another holding solution, is a 409. - Asynchronous forms.
StageAndUpgradeAsyncchecks only what is cheap at request time (the file, the privilege, the options, the name). A refusal that needs the whole plan, such as data loss or a dependency, fails the asynchronous job (asyncoperationstate 3, status 31 / failed, with the message) and changes nothing.
Uninstall. Deleting a managed solution row (SDK, Web API), UninstallSolutionAsync and
POST /admin/solutions/{uniquename}/uninstall remove the solution in one transaction: its holding solution first (if
any), then the solution's layers. A component with no managed and no System layer left is deleted (also if an Active marker
is still on it); a component whose top layer was removed is restored from the layer now on top. Then the
solutioncomponent rows and the solution row go; the publisher stays.
- An uninstall is refused with
0x8004F01Dwhen another managed solution has a layer on a component whose lowest layer is this solution's and that has no System layer (a component this solution created and another one builds on). The message names the component and the other solution. A component that exists in the base system (it has a System layer) never blocks an uninstall. - A holding solution can be uninstalled directly; that cancels the pending upgrade and the base stays as it was.
Data is never dropped silently. This is a deliberate difference from Dataverse. Before an uninstall, a promote
(DeleteAndPromote, StageAndUpgrade) or an upgrade removes a table or a column, xrmverse lists every table to be dropped
that has at least one row and every column to be dropped that has at least one non-null value. If the list is not empty,
SDK, SOAP and Web API requests are refused with 400 naming each table and column and the Admin command that confirms; nothing
changes. Only the Admin API (dropData: true), xrmd solution uninstall --drop-data and the web admin dialog, after
showing the plan, drop the data. dryRun: true returns the plan without writing. The confirmation can carry what the
dry run showed (expectedDataLoss, the web admin always sends it): when the plan made under the lock drops a table or column
that is not in it (a row written since the dry run, say), the request is a 409 and nothing changes; review the plan
again. The counts are taken in the operation's transaction with a shared table lock held to the commit, so no row can be
added between the check and the drop. If you rely on Dataverse's behavior of deleting the data with the solution, expect
the first request to fail.
Managed properties. The flags a managed file carries (IsCustomizable, IsRenameable, CanBeDeleted,
CanModifyRequirementLevelSettings, the table flags CanCreateAttributes, CanCreateForms and CanCreateViews, and the
audit setting of Auditing) are honoured on components with a Managed layer: a customization that would
break one is a 400 naming the flag and the component. The flags are read from the payload of the top Managed layer of
the component.
IsCustomizablefalse on a table, orCanCreateAttributesfalse, refuses a new column (CreateAttribute, the Web APIPOST …/Attributes, a lookup ofCreateOneToManyorCreateCustomerRelationships) and a new relationship (CreateOneToManyon either side,CreateManyToMany) on that table; an unmanaged import that adds a column or a relationship to it is refused whole.CanCreateFormsfalse refuses theCreateof asystemformwhoseobjecttypecodeis the table;CanCreateViewsfalse refuses theCreateof asavedquerywhosereturnedtypecodeis the table. OnlyCreateis checked (see Known limits).- The other
CanModify*flags guard properties xrmverse never lets anyone change, so they hold by construction.CanCreateChartsis read but has nothing to guard (see Known limits). The flags of an unmanaged component are informative only.
Managed components and solutions.
- Deleting a managed component directly (
DeleteEntity,DeleteAttribute, a rowDelete, …) is a 400 naming its solution: uninstall the solution. AddSolutionComponentandRemoveSolutionComponenton a managed solution are a 400; adding a managed component to an unmanaged solution works.- Updating a managed
solutionrow through the SDK or Web API is a 400; only the messages above change it. ismanagedof asolutionrow is set by no request: aCreateorUpdatethat gives it (any value) is a 400. Only an import of a managed file makes a solution managed. ACreateor rename that gives a row the name{name}_Upgradeof an installed managed solution{name}is a 400 too: onlyHoldingSolutionandStageAndUpgradecreate a holding solution.ExportSolutionof a managed solution is a 400 (see Known limits).- Reads report the state:
IsManagedof tables, columns, relationships and choices, and the read-onlyismanagedcolumn of the row component tables (savedquery,systemform,webresource,role,sdkmessageprocessingstep, …). - Internal operations (restoring a layer, uninstalling, promoting) bypass these guards: they must be able to change what
a user may not. An uninstall's deletes need the caller's Delete privilege on each component's table (a promote needs
prvImportCustomization); restores are metadata updates and are not privilege-checked on their own.
Restore. When a layer goes away, the component is rebuilt from the payload of the layer below it, through the importer
in a restore mode (no solution, solutioncomponent or importjob rows, no Active marking).
- A restore replaces labels: the label set of the restored layer replaces the current one; a language that only the removed layer had disappears.
- An option value that an upgrade removed from a choice is kept (rows may use it), and the result carries a warning.
- Uninstall and promote return the warnings of their restores, as entries of
kept(component and reason) in the Admin API result, the CLI output and the web admin. - A single restore whose layers were captured in different languages is a 400 naming both languages and the component; nothing changes.
- A difference the store cannot apply (a shorter
MaxLength, another attribute type) fails the whole operation, as an import does. A difference in a relationship's cascade behavior is not reverted; the restore logs a warning naming the relationship. - The system columns of a table that a managed file lists (primary key,
createdon, …) are layered like any other column, so they come back too, though they are not rows ofsolutioncomponent.
Surfaces
- Web API actions and the SDK messages: the table at the top of this page.
- Admin API:
POST /admin/solutions/{uniquename}/uninstallandPOST /admin/solutions/{uniquename}/promote, body{ "dropData": false, "dryRun": false, "expectedDataLoss": [{ "table": "…", "column": null }] }(all optional;expectedDataLosstakes the dry run'sdataLossentries, theirrowsignored; 409 when the plan under the lock drops anything else), response{ deleted[], restored[], kept[], dataLoss[] }(keptalso carries the warnings of restores);GET /admin/solutions/{uniquename}/layersandGET /admin/layers/{componenttype}/{objectid}list layers (solution, kind, order, version). - CLI:
xrmd solution import <file> --mode update|upgrade|stage(defaultupdate) and--overwrite;xrmd solution uninstall <name> [--drop-data] [--dry-run] [--yes];xrmd solution promote <name> [--drop-data] [--yes];xrmd solution layers <name>andxrmd solution layers --type <t> --id <id>. Animport --mode upgradethat data loss refuses names--mode stagefollowed bypromote --drop-dataas the way forward. - MCP: the read-only tool
list_component_layers(by solution or by component);list_solutionsshowsholding. There is no destructive solution tool. - Web admin, Solutions page: managed and holding badges, an import mode selector, Uninstall and Apply upgrade dialogs that show the plan and require a checked box when data would be lost (the confirmation sends the loss shown; on a 409 the dialog loads the new plan), and a Layers panel listing the layers a solution owns.
Import
- Order of application (§7): publisher → global option sets → entities (non-lookup attributes) → relationships and lookups →
roles and privileges →
savedquery/systemform/sitemap/app module as opaque XML → web resources (stored, not served) →pluginassembly→plugintype→ custom APIs → processes (workflow, below) →sdkmessageprocessingstepand images →solutioncomponent. - All in one SQL transaction: an error midway also rolls back tables and columns already created; the metadata, security
and step caches stay unchanged. The
importjobrow is written after the rollback, in its own transaction, withprogress0 and the XML error log. Dependencies (entities, attributes, option sets referenced and absent from both the package and the org) are checked before writing: 400 naming the missing component. - A solution with
<Managed>1</Managed>is imported as managed: install, update or stage, by the rules of Managed solutions and layers above. The Microsoft first-party warning applies to it exactly as to an unmanaged one. A solution file that is a patch (a parent-solution element insolution.xml, or a unique name ending_Patch_and eight hex digits) is a 400 before anything is written; patches are not supported yet. - Currencies are data, not solution components: a solution never carries
transactioncurrencyrows. A money column in the file bringstransactioncurrencyidandexchangeratewith it (so the old warning that these system attributes are not created is gone), and theTransactionCurrency_*relationship in the file is not imported, as system relationships are derived. An export writes the pair and the_basecolumns. An import applies the labels of a_basecolumn from the file (an unmanaged import over a managed, non-renameable money column is refused); see Currencies. - Re-import (same or higher version) = idempotent, additive upgrade, like Dataverse's unmanaged: new columns and
options added, labels updated, nothing deleted. On system attributes labels, required
level and state options are applied; any other difference from those generated by xrmverse is a warning in the log naming the
property. On custom attributes and relationships a difference that the store cannot apply is 400 (component and
property); a relationship with the same
SchemaNamebut different entities is 400. - Privileges:
prvImportCustomizationis enough; components are applied without per-component checks (Dataverse checks them one by one), except for the system table rules (standard roles and roles with a template → 400; a step withimpersonatinguseridrequires the importer's directprvActOnBehalfOfAnotherUser). - Roles: created in the root BU with their
roleid; privileges are resolved by name, an unknown one is a warning and is skipped. - Steps: messages are resolved from the Dataverse
sdkmessageids (name ↔ id table forCreate,Update,Delete,Retrieve,RetrieveMultipleandAssociate, the ids observed in a real export) or by name for custom actions; an unknown id is a 400 naming it. Filters are resolved from message +PrimaryEntity; the export writes theSdkMessageFilterIds of the xrmverse catalog. - Custom APIs (
customapis/, component type 9990): applied after the plug-in types and before the steps, so a step that xrmverse exported on an API's message imports back (a step on a global API's message keeps itsnonefilter only when the API is in the same file); a step of a Dataverse export on an API's message is a 400 (its message id cannot be mapped). Validation, re-import rules and export: see Custom APIs. - Processes (
<Workflows>, component type 29, from memory): a<Workflow>of category 0 (workflow), 2 (business rule) or 3 (action) becomes aworkflowdefinition row with itsprocesstriggerrows and the XAML of itsXamlFileName(a missing XAML file is a 400 naming the file and the process); the element itself is kept, so the export writes it back with the changed values in place and the XAML byte for byte. The process is saved Draft, then activated when the file'sStateCodeis 1 or whenPublishWorkflowsis true. An activation the server refuses is a warning in the import job log (groupworkflows), and the process stays Draft; steps xrmverse does not run are a warning naming the process and each step, the process keeps its state and fails when triggered, and the rest of the solution imports. AStateCodeother than 0 or 1 is a warning and the process stays Draft. An action gets its message at import (see Classic workflows); a step in the same file on that message resolves to it. Processes of category 1 and 4–7 (dialogs, business process flows, modern, desktop and AI flows) and a<Workflow>withoutCategorystay preserved XML, with a warning "kept for export but not run"; when the file has no process of category 0, 2 or 3, the whole node is kept unchanged. Only definitions are solution components: activation rows are not tracked, andAddSolutionComponentof an activation is a 400. Processes take part in managed layers (sectionWorkflows/Workflow, keyed by the process id), and an uninstall removes them after the steps and cancels their waiting jobs. See Classic workflows. A process written as YAML (xrmd workflow apply --solution <unique name>, see Workflow authoring as code) is added to an unmanaged solution as component type 29 like an imported one, and the export carries it: for a process without kept XML the export writes the<labels><steplabels>of its business rule step labels and the<Descriptions>, as the real export's rule has them, so an authored rule exports like an imported one. - Unknown elements of supported components and nodes of unsupported components (ribbon, report, template, processes of the categories above that do not run, …): see Preserved XML below.
Request options
| Request | Accepted | Rejected (400 naming the option) |
|---|---|---|
ImportSolution, ImportSolutionAsync, ExecuteAsync(ImportSolution) |
CustomizationFile, ImportJobId; HoldingSolution (managed files only: stage); OverwriteUnmanagedCustomizations (removes the Active markers of the file's components, managed files; no effect on an unmanaged file); PublishWorkflows (activates every process of category 0, 2 and 3 in the file whose StateCode is 0 or 1); SkipProductUpdateDependencies, SkipQueueRibbonJob, AsyncRibbonProcessing with any value (there is nothing to skip) |
HoldingSolution true on an unmanaged file, ConvertToManaged true, non-empty ComponentParameters or SolutionParameters, IsTemplateMode true, non-empty TemplateSuffix/TemplateDisplayNamePrefix, any LayerDesiredOrder, SchemaUpdatesOnly true (Web API only), StageAndUpgrade as a parameter (Web API only: use the StageAndUpgrade action) |
StageAndUpgrade, StageAndUpgradeAsync, ExecuteAsync(StageAndUpgrade) |
CustomizationFile, ImportJobId, OverwriteUnmanagedCustomizations, PublishWorkflows, SkipProductUpdateDependencies, SkipQueueRibbonJob, AsyncRibbonProcessing, as for ImportSolution; a managed file only |
an unmanaged file, ConvertToManaged true, non-empty ComponentParameters or SolutionParameters, any LayerDesiredOrder; HoldingSolution, SchemaUpdatesOnly, IsTemplateMode, TemplateSuffix, TemplateDisplayNamePrefix are not parameters of the action (Web API 400) |
ExecuteAsync |
Request = ImportSolutionRequest, StageAndUpgradeRequest or DeleteAndPromoteRequest |
any other request, also Merge |
DeleteAndPromote, DeleteAndPromoteAsync |
UniqueName (a solution with a holding solution {name}_Upgrade) |
any other parameter; no holding solution 400 |
UninstallSolutionAsync |
SolutionUniqueName |
any other parameter; Default, Active, an unmanaged solution |
CloneAsPatch, CloneAsSolution |
— | always 400 (patches are not supported yet) |
ExportSolution |
SolutionName (an unmanaged solution), Managed false, every Export* false |
Managed true, a managed solution, an Export* true, TargetVersion, ExportComponentsParams, any other parameter; system solutions; a step on a message whose Dataverse id xrmverse does not know |
RetrieveFormattedImportJobResults |
ImportJobId (404 if unknown) |
— |
AddSolutionComponent |
ComponentId, ComponentType (types tracked by xrmverse), SolutionUniqueName, DoNotIncludeSubcomponents (rootcomponentbehavior 1 on an entity), AddRequiredComponents (adds the attributes of an entity; no-op for option sets, web resources, sitemaps, plug-in assemblies) |
non-empty IncludedComponentSettingsValues, AddRequiredComponents on the other types (xrmverse does not compute dependencies), an untracked type; nonexistent component 404 |
RemoveSolutionComponent |
SolutionComponent/ComponentId, ComponentType, SolutionUniqueName |
removal from Default; component not present in the solution 404 |
Via Web API a parameter the action does not have, a value of the wrong type or a null for a non-nullable parameter is
a 400 naming the parameter (the error code Dataverse uses for unknown parameters is not verified). A JSON body
with duplicate keys is a 400 (Invalid request body: …), for every Web API endpoint. A non-nullable parameter
missing from the body is not rejected: a boolean counts as false, like the default value of the SDK request (not
verified that Dataverse accepts it instead of rejecting it).
Asynchronous import: ImportSolutionAsync and ExecuteAsync write an asyncoperation row with operationtype 203
("Import Solution Async Operation", Learn), messagename ImportSolution, the file in data (base64) and the importjobid
in the xrmverse_importjobid column (importjob is not a regardingobjectid target on Learn). The background worker
runs it as the user who queued it: 3/30 on successful import, 3/31 on failed import; data is cleared when the
job ends. Poll asyncoperation(id) until statecode 3. The worker runs the import inline, in the same
loop that runs the asynchronous plug-in jobs: while an import is running, queued asynchronous plug-in jobs wait.
Export: the components are the solutioncomponent rows of the solution; metadata come from the cache, rows from the
database, preserved XML is re-emitted in its place. The customizations.xml of a just-imported solution, normalized,
matches the original; the plug-in assembly comes out byte for byte identical. An entity with behavior 0 is written with all
its attributes, custom relationships and its views and forms; with behavior 1 and 2 with primary key, primary name and
the attributes the solution contains: behavior 1 and 2 export identically (xrmverse does not keep separate
table metadata). System relationships are not written (xrmverse derives them from lookups with their own names); global
option sets not contained in the solution become MissingDependency.
Preserved XML: what a solution file provides and xrmverse does not model is not silently lost.
- Nodes of unsupported components (ribbon, report, template, processes of category 1 and 4–7, …), the solution names in all languages and
the publisher details: opaque XML in the
solutionrow (xrmverse_unsupportedxml), re-emitted on export. - Archive entries that xrmverse does not read (the folders of recent exports such as
bots/,botcomponents/,environmentvariabledefinitions/,Formulas/, and theWorkflows/andCanvasApps/files of unsupported components; the XAML files of processes of category 0, 2 and 3 are read, not kept): kept byte for byte in the same column and written back at the same path on export; the import logs one warning per top-level folder. They have no effect in xrmverse. Thecustomapis/folder is read and applied (see Custom APIs); a file under it that is not one of the three custom API file paths is still kept this way, with the warning. MissingDependenciesofsolution.xmlare not verified: xrmverse checks itself that the tables and choices the components use exist, and the import logs one warning with the number of declared dependencies.- Unmodeled children of entities, attributes, relationships, option sets, web resources, steps, images, assemblies, views, forms
and sitemaps (search flags, managed properties,
DisplayMask,SecurityTypes, …): preserved as opaque XML in the original position (meta.*.unmodelled_xml,xrmverse_componentxmlcolumn of the system tables) and re-emitted on export. They have no effect in xrmverse and are not visible via SDK or Web API. The exception isIsAuditEnabledof tables and columns and the table'sCanModifyAuditSettings, which are modelled (see Auditing), and the table'sAutoCreateAccessTeams(below). The associated menu configuration of relationships is preserved the same way. - The internal opaque columns are written only by the import:
xrmverse_componentxmlof every table,solution.xrmverse_unsupportedxmlandappmodule.descriptor(read-only in Dataverse). A Create/Update via SDK or Web API that contains one is a 400 naming the column; app modules can still be created via SDK, withoutdescriptor. A preserved node that has the name of a root node written by xrmverse (Entities,AppModules,Languages, …) is an explicit export error (500 naming it), never a replacement. - The elements the normalizer ignores in the comparison are only those of form (
IntroducedVersion,CustomizationLevel, order of keyed sets, name case, empty labels, …). - An entity that the file carries only as a "shell" (not contained, or behavior 2) does not overwrite the preserved settings; the log flags it with a warning naming the component.
DeleteSolution (user decision of 2026-11-08, option C): the Delete of a solution row (SDK Delete,
Web API DELETE solutions(id)) deletes only the container, like Dataverse for an unmanaged solution: the solution row
and its solutioncomponent rows disappear, every component remains (tables, columns, data, roles, steps,
assemblies, web resources) and is tracked by Default; the publisher remains. The Delete of a managed row is an
uninstall (see Managed solutions and layers): its components are removed, restored or kept according to their layers,
and data is never dropped without the explicit Admin confirmation. The unmanaged cascade (delete the components of an unmanaged
solution, dropping tables and data) is available only through an explicit xrmverse command (xrmd CLI / Admin API): never
via SDK or Web API. The Admin cascade on a managed solution is a 400 that points to uninstall. Default and Active are
rejected with 403. The dependency refusal 0x8004f01d exists only for the uninstall of a managed solution with a layer above
(see above); an unmanaged delete has none.
Size limit
- A
CustomizationFile(decoded) over 95 MB, Dataverse's maximum for a solution file, is rejected byImportSolution,ImportSolutionAsyncandExecuteAsync(ImportSolution), via SOAP and Web API, before writing anything: 4000x80040375(ImportFileTooLargeToUpload), "The import file is too large to upload." (code and message from Learn). The limit is 95 × 1024 × 1024 bytes: Learn says "95 MB" without specifying the unit. - The uncompressed content of the zip is limited to 100 MB in total (
SolutionZip.MaxUncompressedBytes, zip bomb defense): beyond that it is 400 "The solution file is larger than 100 MB uncompressed.". Dataverse does not have this limit: a valid file of 95 MB compressed that expands beyond 100 MB is explicitly rejected by xrmverse. - The routes that carry a solution file (SOAP endpoint, Web API
ImportSolutionandImportSolutionAsync) accept requests up to 128 MB (134,217,728 bytes), Dataverse's maximum for a request, so a 95 MB file fits once base64-encoded. Beyond that: 413 (via SOAP a fault with status 413). The other routes stay at Kestrel's default limit (30,000,000 bytes, 413 beyond), lower than Dataverse's 128 MB. - The SOAP endpoint does not know the message before reading the body: the 128 MB limit and the array quota
(
byte[]up to 128 MB) apply to every SOAP message, not only the import, and the body is read entirely into memory before deserialization. Memory per SOAP request can therefore reach a few hundred MB (body plus deserialized objects); the buffer is not preallocated fromContent-Length(a client could declare 128 MB without sending it). - A large import holds in memory, per request, the file, the decompressed entries (up to 100 MB), the XML documents and the parsed package: the peak can reach a few hundred MB per import, and there is no limit on the number of concurrent imports (no gate): several large imports together add up their memory.
Existing databases: on startup an existing database receives the migrations (0002–0008, meta.* columns; 0008 adds
meta.component_layer and meta.solution_rank; the row component tables get a read-only ismanaged column, and rows that
existed before count as unmanaged; before
1.0 the old 0003_solution_settings was merged into 0003_unmodelled_xml: a development database that had
applied only the old 0003 must be recreated) and the
new columns of the system tables (xrmverse_importjobid, xrmverse_componentxml), so import, export and asynchronous
import work. It does not, however, receive changes to columns already present or the new privileges of the standard roles:
asyncoperation.operationtypedoes not have the options 203, 207, 208 and 211: those jobs are written and read normally, but without a label (no formatted value such as "Import Solution Async Operation");the
asyncoperation.datametadata keepMaxLength1,048,576 instead of 1,073,741,823 (the SQL column isnvarchar(max)and the asynchronous import writes it anyway):RetrieveEntity/EntityDefinitionsshow the old value;System Customizer does not have the new privileges (metadata,
prvImportCustomization,prvExportCustomization,prvPublishCustomization, solution tables): standard role privileges are seeded only when the role row is created (see Security), so a System Customizer receives 403 on these messages (System Administrator does not, because it has everything by template).a database created before Plan 5 has no
solutioncomponentrows for the components created earlier (tables, columns, steps, …):Defaultis incomplete and an export or a query onsolutioncomponentdoes not see them until they are added withAddSolutionComponent.
To align a database created before 1.0 there are two ways: recreate it (empty database, new full seed),
or keep it and manually grant the missing privileges to the System Customizer role with AddPrivilegesRole (the two
asyncoperation metadata differences remain).
Limits and differences
- Known limits of managed solutions (each is refused explicitly or documented here):
- No patches (a later item):
CloneAsPatch,CloneAsSolutionand a patch file are 400. - No managed export (a later item):
ExportSolutionwithManagedtrue, or of a managed solution, is 400. xrmverse cannot produce managed files. - Forms, site maps and app modules are not merged between layers: the top layer wins and the hidden one is a log warning.
- A holding solution is recognized by the
_Upgradesuffix of its unique name only. - A
StageAndUpgradeof the same version as the installed one is refused (checked against Dataverse in the checklist). - Writes to
fieldpermissionrows do not put an Active marker on the component. GETlayers of a component that exists but was never layered returns 404.- An unmanaged customization of a seeded component made before the first managed solution layered it becomes part of that component's System layer; xrmverse cannot tell it apart.
RemoveActiveCustomizations, themsdyn_componentlayerandmsdyn_solutioncomponentsummaryvirtual tables,RetrieveDependenciesForUninstall,RetrieveDependentComponentsand dependency tracking beyond layers are not available (layers are readable through the Admin API, CLI, MCP and web admin).LayerDesiredOrder,ConvertToManaged, templates andSolutionParametersstay refused.- Data loss is refused on SDK, SOAP and Web API paths (deliberate, see above).
CanCreateChartsis read from a managed table but not enforced: xrmverse has no chart table (savedqueryvisualization), so there is no chart to create.CanCreateFormsandCanCreateViewsare checked onCreateonly: anUpdatethat moves an existing form (objecttypecode) or view (returnedtypecode) onto a table whose flag is false is not refused.- The SDK or Web API
Deleteof a managedsolutionrow is its uninstall: it runs outside the pipeline, so no plug-in step registered on the Delete ofsolutionfires, and the audit records the deletes of the uninstall (the solution row and its components) as the internal caller, not as the user who asked. - The restores of an uninstall or a promote are not privilege-checked on their own (internal operations bypass the customization guards, ruling R2); only the deletes need the caller's privileges.
- No patches (a later item):
- Views, forms, sitemaps and app modules are opaque XML without validation: an invalid view or form is imported without error. Web resources are stored but not served.
asyncoperation.regardingobjectidremains aUniqueIdentifier(not navigable).- In
$metadatathe metadata entity sets (EntityDefinitions,RelationshipDefinitions,GlobalOptionSetDefinitions, …) are minimal open types (key and name) andcrmbaseentityis not their base type: a client that generates proxies from the CSDL gets them incomplete (ServiceClientusesRetrieveEntityand is not affected). - The referencing navigation property of a custom 1:N relationship is its
ReferencingEntityNavigationPropertyName(default: the lookup'sSchemaName,xv_ProjectId, as Dataverse) in$metadata,$expand,@odata.bindand the retrieved relationship;@odata.bindaccepts exactly that name. A re-import does not change the name of an existing relationship. - A Customer column (
contact.parentcustomerid, or one made withCreateCustomerRelationships) has one navigation property per target in$metadata,@odata.bindand theassociatednavigationpropertyannotation:parentcustomerid_accountandparentcustomerid_contact(theReferencingEntityNavigationPropertyNameof each relationship). A bind through the other table's name, through both names in one body, or through the bareparentcustomerid@odata.bindfails with 400 naming the valid names.$expandof a Customer column is not supported (as for any polymorphic lookup).owneridand the other multi-target lookups keep one navigation property. A Customer column is created only withCreateCustomerRelationships(SDK orPOST CreateCustomerRelationships), for exactlyaccountandcontact. The Web API action takes the body of the Microsoft documentation as it is: aLookuptypedComplexLookupAttributeMetadata(orLookupAttributeMetadata) and, when it gives aSchemaNameand noLogicalName, theSchemaNamein lower case as the logical name (only this action derives it; the other creation paths need aLogicalName). On its relationships Delete is Cascade, RemoveLink (the default) or Restrict and Merge is Cascade (the default; the only valid option on Dataverse foraccountandcontact) or NoCascade; any other Delete or Merge value is refused with 400, and Assign, Share, Unshare, Reparent, Archive and RollupView are stored as given (not enforced, see "Delete cascades"), so a solution exported from Dataverse with a Customer column imports. The action is not declared in$metadata. - Delete cascades. The Delete behavior of every saved one-to-many relationship (seeded, created with
CreateOneToManyorCreateCustomerRelationships, or imported) is enforced in the transaction of theDelete: Restrict refuses theDeletewith 400 naming the relationship while records point to the record (a record pointing to itself, or one being deleted by the same cascade, does not count); Cascade deletes the records that point to it first, each as aDeletemessage of the pipeline (the privilege check of the caller on that record, its plug-in steps with the parentDeleteasParentContextat depth + 1, its files, its shares and its own cascades), so a chain deeper than the pipeline's maximum depth (8) fails with the infinite-loop error, and a cycle or self-reference is deleted once; RemoveLink clears the lookup (and the type column of a Customer column) of the records that point to it with a direct update:modifiedonandmodifiedbydo not change, no plug-in runs for those records. Whether Dataverse runs the cascaded deletes' plug-in steps at depth + 1 and checks the caller's privileges on each child is not verified against a real Dataverse (see the accounts and contacts checklist). With auditing on, each record deleted by a Cascade gets its own Delete audit row (when its table is audited); the lookups cleared by RemoveLink are not audited (see Auditing, Known limits). A failure anywhere rolls the wholeDeleteback. Relationships implied by a lookup without a saved relationship (system lookups such asownerid) have no cascade. - Known gap: the Assign, Share, Unshare, Reparent and Merge cascades are not enforced. They are stored, imported,
exported and reported by the metadata reads, but
Assign,GrantAccess/ModifyAccess/RevokeAccessand a change of parent do not cascade to the child records, andMergeitself is refused. Code that relies on assigning or sharing an account to also assign or share its contacts sees different data than on Dataverse. CreateEntity/UpdateEntity/UpdateAttributereject (400) the properties that xrmverse does not model or apply (see the metadata write rules above). On column creation the flags that the export writes from the default model are checked, and so are the other column properties the export does not write (IsValidForAdvancedFind,IsValidForForm,IsValidForGrid,IsValidForReadother than true; any value ofCanBeSecuredFor*,DeprecatedVersion,InheritsFrom,ColumnNumber,LinkedAttributeId): they are rejected with 400.- A metadata write holds a pool connection open while waiting for the metadata lock (one write at a time per process): many concurrent metadata writes can exhaust the connection pool.
- Web API
$ref: an unknown entity set in the request URL ({set}({id})/{nav}/$ref) is 404, in the reference URL (@odata.idof the body or$id) it is 400.
Not yet verified against a real Dataverse export (deferred to a dedicated step with a real solution, decision
of 2026-11-08; today the verification uses the hand-written fixture tests/fixtures/solutions/XrmverseFixture):
- the Dataverse
sdkmessageids (onlyCreateandUpdatemapped); - the shape of
AppModule(componenttype 80 included); - the
importjoblog format; - the form type mapping (
systemform.type↔forms/@type); - the shape of
MissingDependency; - the SDK default values that the export writes for the unmodeled properties of components created in xrmverse;
- the columns of the SpreadsheetML workbook of
RetrieveFormattedImportJobResults; - the meaning of the
idreturned byRemoveSolutionComponent; - the error code for an unknown parameter of a Web API action.
- whether Dataverse accepts (as
false) or rejects a non-nullable boolean parameter missing from the body of a Web API action.