← Microsoft DataverseCONTENT HISTORY

Update to Microsoft Dataverse

Snapshot Sep 30, 2026 · 23:13 UTC · version 1.11.3

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full 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