使い方
- インストールコマンドをコピーし、自分の環境へ導入する前にソースを確認します。
- 必要なツール、認証情報、対応環境を原文で確認します。
- 入力と成果物を指定し、出力を資料と照合します。
npx skills add https://github.com/microsoft/dataverse-skills --skill dv-solution開始前の確認
- 導入にはモデル契約、API枠、ファイルへのアクセス許可は含まれません。
- スター数や導入数は活動の指標であり、独立した品質・安全性試験ではありません。
見出し、例、コード、表、リンク、参照画像を含む原文を表示しています。
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/unpackare local file operations (no auth) but need a host that can run PAC -- do them on a capable machine or CI runner. Verify egress withpython scripts/auth.py --check. Seedv-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:
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):
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:
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):
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. Runpac 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 UnmanagedWindows 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>.zipCommit:
git add ./solutions/<UniqueName>
git commit -m "chore: pull <UniqueName> baseline"
git pushPush: Pack + Import
Pack the source files back into a zip:
pac solution pack \
--zipfile ./solutions/<UniqueName>.zip \
--folder ./solutions/<UniqueName> \
--packagetype UnmanagedImport (async recommended for large solutions):
pac solution import \
--path ./solutions/<UniqueName>.zip \
--environment <url> \
--async \
--activate-pluginsPoll 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
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
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 createCheck a view exists
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:
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:
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
jobs = client.records.list(
"importjob",
select=["importjobid", "solutionname", "startedon", "completedon", "progress"],
orderby=["startedon desc"],
top=5,
)For detailed error history, also query msdyn_solutionhistory:
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 Unmanagedfor the development solution. Managed packages are for deployment to downstream environments (test, prod). --activate-pluginsensures any registered plugins in the solution are activated on import.- If you see "solution already exists" errors, use
--import-mode ForceUpgradeto 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.pyfor credential/token acquisition. Seedv-queryfor SDK query patterns anddv-datafor write patterns.




