← Microsoft DataverseCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Microsoft Dataverse
Snapshot Sep 30, 2026 · 23:13 UTC · version 1.11.3
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "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.",
"included_files": [],
"name": "dv-solution",
"skill_md_contents": "---\r\nname: dv-solution\r\ndescription: 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.\r\n---\r\n\r\n# Skill: Solution\r\n\r\nCreate, export, unpack, pack, import, and validate Dataverse solutions via PAC CLI. Includes post-import validation using the Python SDK.\r\n\r\n> **Headless / restricted-egress hosts**: use the raw Web API (`ExportSolution` / `ImportSolution`) for the online steps. `pac solution pack`/`unpack` are local file operations (no auth) but need a host that can run PAC -- do them on a capable machine or CI runner. Verify egress with `python scripts/auth.py --check`. See `dv-connect/references/headless-hosts.md`.\r\n\r\n## Skill boundaries\r\n\r\n| Need | Use instead |\r\n|---|---|\r\n| Create tables, columns, relationships, forms, views | **dv-metadata** |\r\n| Create, update, or delete data records | **dv-data** |\r\n| Query or read records | **dv-query** |\r\n| Connect to Dataverse / set up MCP | **dv-connect** |\r\n\r\n---\r\n\r\n## Create a New Solution\r\n\r\n**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.\r\n\r\n### Step 1: Find or Create the Publisher\r\n\r\nEvery 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.\r\n\r\n**Never use the default `new` prefix.** It provides no organizational identity, risks naming collisions, and signals the developer did not follow best practices.\r\n\r\n**Discovery flow — always run this before creating a publisher:**\r\n\r\n```python\r\nimport os, sys\r\nsys.path.insert(0, os.path.join(os.getcwd(), \"scripts\"))\r\nfrom auth import get_client\r\n\r\n# get_client sets a plugin attribution context on the User-Agent header.\r\n# Do not modify the context value — it is a closed schema for server-side\r\n# telemetry (app/skill/agent). Never include secrets or PII.\r\nclient = get_client(\"dv-solution\")\r\n\r\n# 1. Query for existing non-Microsoft publishers\r\npublishers = client.records.list(\r\n \"publisher\",\r\n filter=\"customizationprefix ne 'none' and uniquename ne 'MicrosoftCorporation' and uniquename ne 'Microsoftdynamic'\",\r\n select=[\"publisherid\", \"uniquename\", \"friendlyname\", \"customizationprefix\"],\r\n top=10,\r\n)\r\n\r\nif publishers:\r\n # Show existing publishers and ask user which to use\r\n print(\"Existing publishers in this environment:\")\r\n for p in publishers:\r\n print(f\" {p['uniquename']} (prefix: {p['customizationprefix']}_)\")\r\n # ASK THE USER: \"Which publisher should this solution use?\"\r\n # Or: \"Should I reuse '<name>' (prefix: <prefix>_)?\"\r\n publisher_id = publishers[0][\"publisherid\"] # after user confirms\r\nelse:\r\n # No custom publisher exists — ASK THE USER for prefix\r\n # \"What publisher prefix should I use? (e.g., 'contoso', 'sa', 'lit' — 2-8 lowercase chars)\"\r\n publisher_id = client.records.create(\"publisher\", {\r\n \"uniquename\": \"<publisheruniquename>\",\r\n \"friendlyname\": \"<Publisher Display Name>\",\r\n \"customizationprefix\": \"<prefix>\", # from user input, NOT 'new'\r\n \"description\": \"<description>\",\r\n })\r\n```\r\n\r\n**Rules:**\r\n- **Always ask the user** before creating a new publisher or choosing a prefix. Never hardcode a prefix.\r\n- The prefix must match any tables already created in the solution — you cannot mix prefixes.\r\n- One publisher can own many solutions. Reuse an existing publisher when possible.\r\n\r\n### Step 2: Create the Solution Record\r\n\r\nUse the SDK to create the solution record (preferred over raw Web API):\r\n\r\n```python\r\nimport os, sys\r\nsys.path.insert(0, os.path.join(os.getcwd(), \"scripts\"))\r\nfrom auth import get_client\r\n\r\n# get_client sets a plugin attribution context on the User-Agent header.\r\n# Do not modify the context value — it is a closed schema for server-side\r\n# telemetry (app/skill/agent). Never include secrets or PII.\r\nclient = get_client(\"dv-solution\")\r\n\r\n# Create the solution record\r\nsolution_id = client.records.create(\"solution\", {\r\n \"uniquename\": \"<UniqueName>\",\r\n \"friendlyname\": \"<Display Name>\",\r\n \"version\": \"1.0.0.0\",\r\n \"publisherid@odata.bind\": \"/publishers(<publisher_guid>)\",\r\n})\r\nprint(f\"Created solution: {solution_id}\")\r\n```\r\n\r\nThe required fields:\r\n```\r\nTable: solution\r\nFields: uniquename = \"<UniqueName>\"\r\n friendlyname = \"<Display Name>\"\r\n version = \"1.0.0.0\"\r\n publisherid = <publisher GUID from step 1>\r\n```\r\n\r\n> **Note:** There is no `pac solution create` command. PAC CLI handles export/import/pack/unpack, not solution record creation. Use the SDK or Web API to create the record.\r\n\r\n### Step 3: Add Components\r\n\r\nUse `pac solution add-solution-component` to add tables, forms, views, and other components:\r\n```\r\npac solution add-solution-component \\\r\n --solutionUniqueName <UniqueName> \\\r\n --component <ComponentSchemaName> \\\r\n --componentType <TypeCode> \\\r\n --environment <url>\r\n```\r\n\r\n> **Note:** PAC CLI uses camelCase args here (`--solutionUniqueName`, `--componentType`), not kebab-case.\r\n\r\nCommon component type codes:\r\n| Type Code | Component |\r\n|---|---|\r\n| 1 | Entity (Table) |\r\n| 2 | Attribute (Column) |\r\n| 26 | View |\r\n| 60 | Form |\r\n| 61 | Web Resource |\r\n| 300 | Canvas App |\r\n| 371 | Connector |\r\n\r\nRepeat the command for each component you need to add.\r\n\r\n### Alternative: Auto-add via MSCRM.SolutionName Header\r\n\r\nWhen creating metadata via the Web API, include the `MSCRM.SolutionName` header to auto-add components to the solution:\r\n```python\r\nheaders = {\r\n \"Authorization\": f\"Bearer {token}\",\r\n \"Content-Type\": \"application/json\",\r\n \"MSCRM.SolutionName\": \"<UniqueName>\"\r\n}\r\n```\r\n\r\n**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):\r\n```python\r\nsol = client.records.list(\"solution\",\r\n filter=\"uniquename eq '<UniqueName>'\", select=[\"solutionid\"], top=1).first()\r\nif sol is not None:\r\n components = client.records.list(\"solutioncomponent\",\r\n filter=f\"_solutionid_value eq {sol['solutionid']}\",\r\n select=[\"componenttype\", \"objectid\"])\r\n print(f\"{len(components)} components in the solution\")\r\n```\r\n\r\nIf the header was misspelled or the solution doesn't exist, components will be created in the default solution instead — silently. Always verify.\r\n\r\n## Find the Solution Name\r\n\r\nBefore exporting, confirm the exact unique name:\r\n```\r\npac solution list --environment <url>\r\n```\r\nThe `UniqueName` column is what you pass to other commands. Display names have spaces; unique names do not.\r\n\r\n## Pull: Export + Unpack\r\n\r\n> **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.\r\n\r\nExport the solution as unmanaged (source of truth):\r\n```\r\npac solution export \\\r\n --name <UniqueName> \\\r\n --path ./solutions/<UniqueName>.zip \\\r\n --managed false \\\r\n --environment <url>\r\n```\r\n\r\nUnpack into editable source files:\r\n```\r\npac solution unpack \\\r\n --zipfile ./solutions/<UniqueName>.zip \\\r\n --folder ./solutions/<UniqueName> \\\r\n --packagetype Unmanaged\r\n```\r\n\r\n> **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 `unpack` fails with a lock / \"in use\" error, retry after a moment, and verify the unpacked folder has the expected components before deleting the zip.\r\n\r\nDelete the zip — the unpacked folder is the source:\r\n```\r\nrm ./solutions/<UniqueName>.zip\r\n```\r\n\r\nCommit:\r\n```\r\ngit add ./solutions/<UniqueName>\r\ngit commit -m \"chore: pull <UniqueName> baseline\"\r\ngit push\r\n```\r\n\r\n## Push: Pack + Import\r\n\r\nPack the source files back into a zip:\r\n```\r\npac solution pack \\\r\n --zipfile ./solutions/<UniqueName>.zip \\\r\n --folder ./solutions/<UniqueName> \\\r\n --packagetype Unmanaged\r\n```\r\n\r\nImport (async recommended for large solutions):\r\n```\r\npac solution import \\\r\n --path ./solutions/<UniqueName>.zip \\\r\n --environment <url> \\\r\n --async \\\r\n --activate-plugins\r\n```\r\n\r\n## Poll Import Status\r\n\r\nAfter async import, check the job:\r\n```\r\npac solution list --environment <url>\r\n```\r\n\r\n## Post-Import Validation\r\n\r\nAfter importing a solution, verify that components are live. Use the Python SDK to check directly — no external scripts needed.\r\n\r\n### Check a table exists\r\n\r\n```python\r\ninfo = client.tables.get(\"<logical_name>\")\r\nif info:\r\n print(f\"[PASS] Table '{info.logical_name}' exists\")\r\nelse:\r\n print(f\"[FAIL] Table '<logical_name>' not found\")\r\n```\r\n\r\n### Check a form is published\r\n\r\n```python\r\nforms = client.records.list(\r\n \"systemform\",\r\n filter=\"objecttypecode eq '<entity>' and type eq <form_type_code>\",\r\n select=[\"name\", \"formid\"],\r\n top=5,\r\n)\r\n# Form type codes: 2 = main, 7 = quick create\r\n```\r\n\r\n### Check a view exists\r\n\r\n```python\r\nviews = client.records.list(\r\n \"savedquery\",\r\n filter=\"returnedtypecode eq '<entity>'\",\r\n select=[\"name\", \"savedqueryid\", \"statuscode\"],\r\n top=10,\r\n)\r\n```\r\n\r\n### Check a user's role assignment (N:N `$expand`)\r\n\r\n`records.list` passes `$expand` straight through, so read the N:N navigation property directly with the SDK:\r\n\r\n```python\r\nusers = list(client.records.list(\r\n \"systemuser\",\r\n filter=\"internalemailaddress eq '<email>'\", # fallback: domainname eq '<upn>'\r\n select=[\"fullname\"],\r\n expand=[\"systemuserroles_association($select=name)\"],\r\n top=1,\r\n))\r\nroles = [r[\"name\"] for r in users[0].get(\"systemuserroles_association\", [])] if users else []\r\n```\r\n\r\nAlternatively, the managed **Dataverse CLI** escape hatch (`dataverse api request` — not `urllib`), or FetchXML with a link-entity:\r\n\r\n```bash\r\ndataverse api request --target dataverse --method GET \\\r\n --path \"/api/data/v9.2/systemusers?%24filter=internalemailaddress eq '<email>'&%24select=fullname&%24expand=systemuserroles_association(%24select=name)&%24top=1\" \\\r\n --environment <DATAVERSE_URL> \\\r\n --context \"app=dataverse-skills/<ver>;skill=dv-solution;agent=<agent>\"\r\n```\r\n\r\nThe response `value[0].systemuserroles_association` is the list of assigned roles (each with `name`).\r\n\r\n### Check import errors\r\n\r\n```python\r\njobs = client.records.list(\r\n \"importjob\",\r\n select=[\"importjobid\", \"solutionname\", \"startedon\", \"completedon\", \"progress\"],\r\n orderby=[\"startedon desc\"],\r\n top=5,\r\n)\r\n```\r\n\r\nFor detailed error history, also query `msdyn_solutionhistory`:\r\n\r\n```python\r\nhistory = client.records.list(\r\n \"msdyn_solutionhistory\",\r\n filter=\"msdyn_status eq 1\", # 1 = failed\r\n select=[\"msdyn_name\", \"msdyn_starttime\", \"msdyn_exceptionmessage\"],\r\n orderby=[\"msdyn_starttime desc\"],\r\n top=5,\r\n)\r\n```\r\n\r\n### Validation error reference\r\n\r\n| Error | Cause | Fix |\r\n| --- | --- | --- |\r\n| Table not found after import | Component not in solution | Add via `pac solution add-solution-component` |\r\n| Form check fails immediately | Publishing is async | Wait 30 seconds and retry |\r\n| Role not assigned | User not provisioned | Assign the role via `pac admin assign-user` or the Power Platform Admin Center |\r\n| Import job at 0% | Import still running | Poll again in 60 seconds |\r\n\r\n## Notes\r\n\r\n- Always use `--managed false` / `--packagetype Unmanaged` for the development solution. Managed packages are for deployment to downstream environments (test, prod).\r\n- `--activate-plugins` ensures any registered plugins in the solution are activated on import.\r\n- If you see \"solution already exists\" errors, use `--import-mode ForceUpgrade` to overwrite.\r\n- Large solutions (Sales, Customer Service) can take 10–20 minutes to import. Be patient and poll rather than re-importing.\r\n- All validation queries above require auth. Use `scripts/auth.py` for credential/token acquisition. See `dv-query` for SDK query patterns and `dv-data` for write patterns.\r\n"
}SHA-256 of public snapshot: fd9d0f2c119d8547d7584472ebb82021e45d9fed818524efc96e959a8c779c6a