npx skills add ...
npx skills add microsoft/dataverse-skills --skill dv-solution
npx skills add microsoft/dataverse-skills --skill dv-solution
Dataverse solution lifecycle — create, export, import, promote across environments, and validate deployments. Use when the user wants to package customizations, deploy to another environment, or move work between dev / test / prod.
Create, export, unpack, pack, import, and validate Dataverse solutions via PAC CLI. Includes post-import validation using the Python SDK.
| Need | Use instead |
|---|---|
| Create tables, columns, relationships, forms, views | dv-metadata |
| Create, update, or delete data records | dv-data |
| Query or read records | dv-query |
| Connect to Dataverse / set up MCP | dv-connect |
Use the Python SDK for publisher and solution record creation — not raw HTTP. Publishers and solutions are standard Dataverse tables. client.records.create() and client.records.list() handle auth, pagination, and error handling automatically, avoiding the URL encoding, header boilerplate, and GUID-parsing bugs that raw urllib calls introduce.
Every solution belongs to a publisher. The publisher's customizationprefix (e.g., contoso, sa, lit) is prepended to every custom table, column, and relationship schema name. This prefix is effectively permanent — existing components keep their prefix forever, even if you change the publisher later.
Never use the default new prefix. It provides no organizational identity, risks naming collisions, and signals the developer did not follow best practices.
Discovery flow — always run this before creating a publisher:
Rules:
Use the SDK to create the solution record (preferred over raw Web API):
The required fields:
Note: There is no
pac solution createcommand. PAC CLI handles export/import/pack/unpack, not solution record creation. Use the SDK or Web API to create the record.
Use pac solution add-solution-component to add tables, forms, views, and other components:
Note: PAC CLI uses camelCase args here (
--solutionUniqueName,--componentType), not kebab-case.
Common component type codes:
| Type Code | Component |
|---|---|
| 1 | Entity (Table) |
| 2 | Attribute (Column) |
| 26 | View |
| 60 | Form |
| 61 | Web Resource |
| 300 | Canvas App |
| 371 | Connector |
Repeat the command for each component you need to add.
When creating metadata via the Web API, include the MSCRM.SolutionName header to auto-add components to the solution:
Important: After using this approach, verify components were added by querying the solutioncomponent table with the SDK (pac solution list-components is not available in current PAC):
If the header was misspelled or the solution doesn't exist, components will be created in the default solution instead — silently. Always verify.
Before exporting, confirm the exact unique name:
The UniqueName column is what you pass to other commands. Display names have spaces; unique names do not.
Confirm the target environment before exporting or importing. Run
pac auth list+pac org who, show the output to the user, and confirm it matches the intended environment. Developers work across multiple environments — do not assume.
Export the solution as unmanaged (source of truth):
Unpack into editable source files:
Windows file-lock race. Run export and unpack as separate commands (as above); chaining them immediately can hit a transient ZIP file-lock right after export. If
unpackfails with a lock / "in use" error, retry after a moment, and verify the unpacked folder has the expected components before deleting the zip.
Delete the zip — the unpacked folder is the source:
Commit:
Pack the source files back into a zip:
Import (async recommended for large solutions):
After async import, check the job:
After importing a solution, verify that components are live. Use the Python SDK to check directly — no external scripts needed.
$expand)records.list passes $expand straight through, so read the N:N navigation property directly with the SDK:
Alternatively, the managed Dataverse CLI escape hatch (dataverse api request — not urllib), or FetchXML with a link-entity:
The response value[0].systemuserroles_association is the list of assigned roles (each with name).
For detailed error history, also query msdyn_solutionhistory:
| Error | Cause | Fix |
|---|---|---|
| Table not found after import | Component not in solution | Add via pac solution add-solution-component |
| Form check fails immediately | Publishing is async | Wait 30 seconds and retry |
| Role not assigned | User not provisioned | Assign the role via pac admin assign-user or the Power Platform Admin Center |
| Import job at 0% | Import still running | Poll again in 60 seconds |
--managed false / --packagetype Unmanaged for the development solution. Managed packages are for deployment to downstream environments (test, prod).--activate-plugins ensures any registered plugins in the solution are activated on import.--import-mode ForceUpgrade to overwrite.scripts/auth.py for credential/token acquisition. See dv-query for SDK query patterns and dv-data for write patterns.import os, sys
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
from auth import get_client
# get_client sets a plugin attribution context on the User-Agent header.
# Do not modify the context value — it is a closed schema for server-side
# telemetry (app/skill/agent). Never include secrets or PII.
client = get_client("dv-solution")
# Create the solution record
solution_id = client.records.create("solution", {
"uniquename": "<UniqueName>",
"friendlyname": "<Display Name>",
"version": "1.0.0.0",
"publisherid@odata.bind": "/publishers(<publisher_guid>)",
})
print(f"Created solution: {solution_id}")Table: solution
Fields: uniquename = "<UniqueName>"
friendlyname = "<Display Name>"
version = "1.0.0.0"
publisherid = <publisher GUID from step 1>pac solution add-solution-component \
--solutionUniqueName <UniqueName> \
--component <ComponentSchemaName> \
--componentType <TypeCode> \
--environment <url>headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"MSCRM.SolutionName": "<UniqueName>"
}sol = client.records.list("solution",
filter="uniquename eq '<UniqueName>'", select=["solutionid"], top=1).first()
if sol is not None:
components = client.records.list("solutioncomponent",
filter=f"_solutionid_value eq {sol['solutionid']}",
select=["componenttype", "objectid"])
print(f"{len(components)} components in the solution")pac solution list --environment <url>pac solution list --environment <url>pac solution export \
--name <UniqueName> \
--path ./solutions/<UniqueName>.zip \
--managed false \
--environment <url>pac solution unpack \
--zipfile ./solutions/<UniqueName>.zip \
--folder ./solutions/<UniqueName> \
--packagetype Unmanagedrm ./solutions/<UniqueName>.zipgit add ./solutions/<UniqueName>
git commit -m "chore: pull <UniqueName> baseline"
git pushpac solution pack \
--zipfile ./solutions/<UniqueName>.zip \
--folder ./solutions/<UniqueName> \
--packagetype Unmanagedpac solution import \
--path ./solutions/<UniqueName>.zip \
--environment <url> \
--async \
--activate-pluginsinfo = client.tables.get("<logical_name>")
if info:
print(f"[PASS] Table '{info.logical_name}' exists")
else:
print(f"[FAIL] Table '<logical_name>' not found")forms = client.records.list(
"systemform",
filter="objecttypecode eq '<entity>' and type eq <form_type_code>",
select=["name", "formid"],
top=5,
)
# Form type codes: 2 = main, 7 = quick createviews = client.records.list(
"savedquery",
filter="returnedtypecode eq '<entity>'",
select=["name", "savedqueryid", "statuscode"],
top=10,
)users = list(client.records.list(
"systemuser",
filter="internalemailaddress eq '<email>'", # fallback: domainname eq '<upn>'
select=["fullname"],
expand=["systemuserroles_association($select=name)"],
top=1,
))
roles = [r["name"] for r in users[0].get("systemuserroles_association", [])] if users else []dataverse api request --target dataverse --method GET \
--path "/api/data/v9.2/systemusers?%24filter=internalemailaddress eq '<email>'&%24select=fullname&%24expand=systemuserroles_association(%24select=name)&%24top=1" \
--environment <DATAVERSE_URL> \
--context "app=dataverse-skills/<ver>;skill=dv-solution;agent=<agent>"jobs = client.records.list(
"importjob",
select=["importjobid", "solutionname", "startedon", "completedon", "progress"],
orderby=["startedon desc"],
top=5,
)history = client.records.list(
"msdyn_solutionhistory",
filter="msdyn_status eq 1", # 1 = failed
select=["msdyn_name", "msdyn_starttime", "msdyn_exceptionmessage"],
orderby=["msdyn_starttime desc"],
top=5,
)