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