关于 dv-metadata
dv-metadata 是指导 Agent 在 Dataverse 中创建和修改元数据的技能,面向构建表、列、关系、表单及视图的开发者。它强调先在环境中通过 SDK 或 API 创建,再导出解决方案到代码库,而非手写新组件 XML。
主要功能
- 01先确认目标环境与解决方案
- 02通过 SDK 创建表和列
- 03创建查找字段及表关系
- 04管理表单与视图并发布
- 05将环境变更导出到解决方案
- 06遵循列命名与发布要求
适用场景
- 01为 Dataverse 解决方案添加表与列
- 02建立查找字段及表间关系
- 03修改表单和视图并发布更改
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ microsoft/ dataverse-skills --skill dv-metadata安装后在 Claude Code 中输入/dv-metadata确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/alternate-keys.mdforms-and-views.mdmetadata-propagation.md详细介绍
技能:元数据 — 进行更改
在会话中首次更改元数据之前:
- 向用户确认目标环境 — 请参阅 dv-overview 中的多环境规则。
- 确认解决方案 — 询问“这些组件应归入哪个解决方案?”如果
.env中有SOLUTION_NAME,请确认该值。如果尚无解决方案,必须先向用户询问解决方案名称和发布者前缀,然后才能创建任何内容。发布者前缀是永久的 — 使用该前缀创建组件后就无法更改。
停止并询问用户:
“我应使用什么解决方案名称和发布者前缀?此前缀(例如
contoso、lit、soc)将永久用于每个表和列。”
然后查询现有发布者并展示给用户 — 用户可能想重复使用其中一个:
# Publisher discovery + solution creation — use SDK (never raw Web API).
# See dv-solution for the full publisher discovery flow.
publishers = client.records.list("publisher",
filter="customizationprefix ne 'none' and uniquename ne 'MicrosoftCorporation'",
select=["publisherid", "uniquename", "friendlyname", "customizationprefix"], top=10)
# MANDATORY: Show existing publishers to user and ask which to use or create new
用户确认后,使用 SDK 创建:
publisher_id = client.records.create("publisher", {
"uniquename": "<name>", "friendlyname": "<display>",
"customizationprefix": "<prefix>", # from user input, NOT hardcoded
"description": "<desc>",
})
solution_id = client.records.create("solution", {
"uniquename": "<SolutionName>", "friendlyname": "<Display Name>",
"version": "1.0.0.0",
"[email protected]": f"/publishers({publisher_id})",
})
切勿在解决方案之外创建表或列。
- 在每次 SDK 调用中传入
solution="<UniqueName>",或者在每次原始 Web API 调用中包含"MSCRM.SolutionName": "<UniqueName>"。
技能范围
| 需求 | 改用 |
|---|---|
| 创建、更新或删除数据记录 | dv-data |
| 查询或读取记录 | dv-query |
| 导出或部署解决方案 | dv-solution |
| ERP 架构 | dv-query;ERP 路由 |
如何进行更改:环境优先
不要手动编写解决方案 XML 来创建新表、列、窗体或视图。
环境验证元数据的可靠性远高于由 Agent 编辑 XML。正确的工作流如下:
- 在环境中进行更改,使用 Dataverse MetadataService API(或适用时使用
pac命令) - 通过
pac solution export+pac solution unpack将更改拉取到代码库 - 提交结果
导出的 XML 由 Dataverse 自行生成,始终有效。手写 XML 很脆弱 — 即使只有一个属性不正确或缺少一个元素,也会导致导入失败,并且错误信息晦涩难懂。
只有在编辑代码库中已有文件时才直接编写文件(例如,调整现有视图的列,或修改已拉取到代码库的窗体布局)。
CLI 架构检查: dataverse data describe --table account --include all --json(使用 LogicalName)。API 发现:dataverse api list --target dataverse。
创建表
如果要为数据导入创建多个表,还请参阅本技能后续的以下部分:
- 幂等表创建 — 可重复运行脚本的先检查模式
- 备用键 — upsert 所必需;每创建一个表后立即创建
- 元数据传播延迟和锁争用 — 分阶段创建以避免锁错误
创建表时优先使用 SDK — 只有在需要 SDK 未公开的 OwnershipType、HasActivities 或其他高级属性时,才使用原始 Web API。
SDK 方式(默认使用):
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-metadata")
info = client.tables.create(
"new_ProjectBudget",
{"new_Amount": "decimal", "new_Description": "string"},
solution="MySolution",
primary_column="new_Name",
display_name="Project Budget", # human-readable name; plural auto-appends "s"
)
print(f"Created: {info['table_schema_name']}")
Web API 备用方式(仅当需要 SDK 未公开的 OwnershipType、HasActivities 或其他属性时使用):
# Helper for Label boilerplate
def label(text):
return {"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [{"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": text, "LanguageCode": 1033}]}
entity = {
"@odata.type": "Microsoft.Dynamics.CRM.EntityMetadata",
"SchemaName": "new_ProjectBudget",
"DisplayName": label("Project Budget"),
"DisplayCollectionName": label("Project Budgets"),
"Description": label(""),
"OwnershipType": "UserOwned",
"HasActivities": False, "HasNotes": False, "IsActivity": False,
"PrimaryNameAttribute": "new_name",
"Attributes": [{
"@odata.type": "Microsoft.Dynamics.CRM.StringAttributeMetadata",
"SchemaName": "new_name",
"DisplayName": label("Name"),
"RequiredLevel": {"Value": "ApplicationRequired"},
"MaxLength": 100, "IsPrimaryName": True,
}]
}
# POST to /api/data/v9.2/EntityDefinitions with MSCRM.SolutionUniqueName header
列命名:避免 *Id 后缀冲突
常规列名不得以 Id 结尾(例如 prefix_CountryId)。创建查找列时,Dataverse 会自动生成以 Id 结尾的导航属性 — 如果已存在同名常规列,创建查找列会因架构名称冲突而失败。
- 错误:
prefix_DepartmentId(int)— 与自动生成的查找列冲突 - 正确:
prefix_SrcDepartmentId或prefix_DepartmentSourceId
添加列
SDK 方式(首选):
created = client.tables.add_columns(
"new_ProjectBudget",
{"new_Description": "string", "new_Amount": "decimal", "new_Active": "bool"},
)
print(created) # ['new_Description', 'new_Amount', 'new_Active']
支持的类型字符串:"string" / "text"、"int" / "integer"、"decimal" / "money"、"float" / "double"、"datetime" / "date"、"bool" / "boolean"、"file",以及 Enum 子类(用于本地选项集)。
通过 SDK 添加选项(下拉列表)列:
from enum import IntEnum
class BudgetStatus(IntEnum):
DRAFT = 100000000
APPROVED = 100000001
REJECTED = 100000002
created = client.tables.add_columns(
"new_ProjectBudget",
{"new_Status": BudgetStatus},
)
设置约束——传入 dict 规范,而不是单独的类型字符串(键:max_length、min_value、max_value、precision、format、required、display_name);适用于 create 和 add_columns,无需使用原始 Web API:
client.tables.add_columns("new_ProjectBudget", {
"new_Amount": {"type": "money", "min_value": 0, "max_value": 1000000000, "precision": 2},
"new_Comment": {"type": "memo", "max_length": 2000, "display_name": "Comment"},
})
使用 update_column(table, col, spec) 或 update_columns(table, {col: spec}) 原地修改现有列——使用相同的键;所有规范都会在任何写入操作前完成验证。
查找列和关系
SDK 方式——简单查找(首选):
result = client.tables.create_lookup_field(
referencing_table="new_projectbudget",
lookup_field_name="new_AccountId",
referenced_table="account",
display_name="Account",
solution="MySolution",
)
print(f"Created lookup: {result.lookup_schema_name}")
SDK 方式——完全控制 1:N 关系:
from PowerPlatform.Dataverse.models.relationship import (
LookupAttributeMetadata,
OneToManyRelationshipMetadata,
CascadeConfiguration,
)
from PowerPlatform.Dataverse.models.labels import Label, LocalizedLabel
from PowerPlatform.Dataverse.common.constants import CASCADE_BEHAVIOR_REMOVE_LINK
lookup = LookupAttributeMetadata(
schema_name="new_AccountId",
display_name=Label(localized_labels=[LocalizedLabel(label="Account", language_code=1033)]),
)
relationship = OneToManyRelationshipMetadata(
schema_name="account_new_projectbudget",
referenced_entity="account",
referencing_entity="new_projectbudget",
referenced_attribute="accountid",
cascade_configuration=CascadeConfiguration(delete=CASCADE_BEHAVIOR_REMOVE_LINK),
)
result = client.tables.create_one_to_many_relationship(lookup, relationship, solution="MySolution")
print(f"Created: {result.relationship_schema_name}")
SDK 方式——多对多关系:
from PowerPlatform.Dataverse.models.relationship import ManyToManyRelationshipMetadata
relationship = ManyToManyRelationshipMetadata(
schema_name="new_ticket_knowledgebase",
entity1_logical_name="new_ticket",
entity2_logical_name="new_knowledgebase",
)
result = client.tables.create_many_to_many_relationship(relationship, solution="MySolution")
print(f"Created: {result.relationship_schema_name}")
Web API 方式(SDK 模式无法满足需求时的备用方案):
relationship = {
"@odata.type": "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata",
"SchemaName": "account_new_projectbudget",
"ReferencedEntity": "account",
"ReferencingEntity": "new_projectbudget",
"Lookup": {
"@odata.type": "Microsoft.Dynamics.CRM.LookupAttributeMetadata",
"SchemaName": "new_AccountId",
"DisplayName": {"@odata.type": "Microsoft.Dynamics.CRM.Label",
"LocalizedLabels": [{"@odata.type": "Microsoft.Dynamics.CRM.LocalizedLabel",
"Label": "Account", "LanguageCode": 1033}]},
"RequiredLevel": {"Value": "None"}
}
}
# POST to /api/data/v9.2/RelationshipDefinitions
创建查找列后——@odata.bind 导航属性:
创建记录并设置此查找列时,需要使用 @odata.bind 的导航属性名称。导航属性名称区分大小写,且必须与实体的 $metadata 相符(通常是查找字段的 SchemaName,例如 new_AccountId):
| 导航属性名称 | @odata.bind 键 | 实体集 |
|---|---|---|
new_AccountId | [email protected] | /accounts(<guid>) |
new_ParentTicketId | [email protected] | /new_tickets(<guid>) |
常见错误: 使用小写逻辑名称(如 [email protected])会返回 400 错误。导航属性名称区分大小写,且必须与实体的 $metadata 相符。
向解决方案添加表
通过 API 创建表后,将其添加到解决方案,以便在导出时一并获取:
pac solution add-solution-component \
--solutionUniqueName <SOLUTION_NAME> \
--component <SchemaName> \
--componentType 1 \
--environment <url>
组件类型 1 = Entity(表)。完整类型代码列表请参见 dv-solution。
也可以通过 Web API:
# POST to /api/data/v9.2/AddSolutionComponent
body = {
"ComponentId": "<entity-metadata-id>",
"ComponentType": 1, # 1 = Entity
"SolutionUniqueName": "<SOLUTION_NAME>",
"AddRequiredComponents": True
}
窗体和视图
systemform(窗体)和 savedquery(视图)是普通实体——使用 SDK 的记录 CRUD 创建、读取和修改它们(无需 urllib)。只有发布(PublishXml,一个无绑定操作)需要使用 dataverse api request。
快速参考:
- 创建窗体:
client.records.create("systemform", {...formxml..., "type": 2})(2=主窗体,7=快速创建,6=快速视图,11=卡片)。 - 修改窗体: 使用
records.list("systemform", filter=...)获取模板 → 修改formxml→records.update("systemform", id, {...})→ 发布。 - 发布:
dataverse api requestPOSTPublishXml——要使更改生效,必须执行此操作。 - 创建视图:
client.records.create("savedquery", {...fetchxml..., ...layoutxml...})(0=标准,1=高级查找,2=关联,4=快速查找)。
完整代码、模板编写方法、classid 表格和发布说明请参见:references/forms-and-views.md。
关键规则:
- 窗体 XML 中的所有
id属性都必须是唯一的 GUID(str(uuid.uuid4()).upper())。 - 在 Windows 上不要使用
python -c生成 GUID——请编写.py文件。 - 每次创建或修改窗体后都必须发布,否则用户看不到更改。
业务规则
请在 Power Apps maker portal 中创建业务规则。使用 JSON/XAML 编写业务规则过于复杂,难以确保可靠。创建后,导出并解包解决方案,然后提交结果。
发布者前缀
所有自定义架构名称都必须使用解决方案的发布者前缀(例如 new_、contoso_)。查找前缀:
pac solution list --environment <url>
或者在首次拉取后查看 solutions/<SOLUTION_NAME>/Other/Solution.xml——查找 <CustomizationPrefix>。
FormXml 注意事项
- 所有
id属性都必须是有效的 GUID,格式为{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}。不要使用"general"这样的字符串。 labelid也是 GUID,不能使用便于阅读的字符串。- 子网格控件需要有效的
<ViewId>,且必须是现有 SavedQuery 的 GUID。请先创建视图。 - 整个表单中的单元格、节、选项卡和控件 ID 都必须唯一。
- 控件的
classid值——参见上方的 classid 表格。
提示: 在 maker portal 中创建表单,然后通过 pac solution export 拉取——将拉取的 XML 用作程序化创建的模板。
创建列后:报告逻辑名称
创建列后(通过 Web API 或 MCP),务必向用户报告实际的逻辑名称。列名称可能会以用户意料之外的方式规范化或添加前缀。请用表格汇总:
| 显示名称 | 逻辑名称 | 类型 |
|---|---|---|
| cr9ac_email | String | |
| Tier | cr9ac_tier | Picklist |
| Customer | cr9ac_customerid | Lookup |
这样可以避免用户使用错误的列名称插入数据,导致后续操作失败。
常见 Web API 错误代码
| 错误代码 | 含义 | 恢复方法 |
|---|---|---|
0x80040216 | 暂时性元数据缓存错误。列或表的元数据尚未传播完成。 | 等待 3-5 秒后重试。通常第二次尝试即可成功。 |
0x80048d19 | 负载中包含无效属性。字段名称与表中的任何列都不匹配。 | 检查逻辑列名称——使用 EntityDefinitions(LogicalName='...')/Attributes 进行验证。 |
0x80040237 | 架构名称已存在。 | 创建新列或新表之前,先确认该列或表是否已存在——它可能是先前超时的调用创建的。 |
0x8004431a | 发布者前缀不匹配。 | 确保所有架构名称都使用解决方案的发布者前缀。 |
0x80060891 | 创建表后元数据缓存尚未就绪。 | 先调用 GET EntityDefinitions(LogicalName='...') 以强制刷新缓存,然后重试。 |
向用户显示错误代码之前,务必将其转换为通俗易懂的说明。
元数据传播延迟与锁争用
创建表/列/备用键后,Dataverse 会运行内部元数据操作(索引构建、缓存传播),耗时 3–30 秒。在这些操作运行期间提交其他元数据操作会导致锁争用错误。
缓解方法——分阶段创建,不要交错执行。 创建所有表 → 等待 15–30 秒 → 创建所有备用键 → 等待 15–30 秒 → 创建所有查找列。不要交错执行同一张表上的操作。
症状(出现以下任一情况都意味着传播尚未完成):
- 创建选项集列时出现
0x80040216错误 - Lookup
@odata.bind出现 "Invalid property" 错误 update_table(MCP)出现 "EntityId not found in MetadataCache" 错误- 创建 Lookup 或备用键时出现 "another customization operation is running" 错误
有关可捕获暂时性锁错误的 retry_metadata 助手和完整的分阶段创建流程,请参阅 references/metadata-propagation.md。
检查现有架构
更改模型之前,请检查现有内容。这些只读调用会返回原始元数据字典(PascalCase 名称),可随时安全运行。
假设使用此技能前文所示的身份验证设置中的
client(from auth import get_client)。
# typed=True returns type-specific fields (MaxLength, MinValue/MaxValue) in one request -- no @odata.type cast, no urllib
for col in client.tables.list_columns("account", typed=True):
print(col["LogicalName"], col.get("MaxLength"))
# get_column(table, col, typed=True) reads one column's constraints
# All relationships for one table (1:N, N:1, and N:N combined)
rels = client.tables.list_table_relationships("account")
for rel in rels:
print(f"{rel['SchemaName']} -> {rel.get('@odata.type')}")
# All relationships in the environment (optionally filtered)
all_rels = client.tables.list_relationships(select=["SchemaName", "ReferencedEntity", "ReferencingEntity"])
如需发现可通过 SQL 查询的列(不包括虚拟列/计算列),dv-query 还提供
client.query.sql_columns(table)。
会话结束:拉取到仓库
每次元数据会话结束后,都要执行拉取到仓库的流程——完整的导出/解包/提交命令请参阅 dv-overview 中的“任何更改后:拉取到仓库”。
如果创建时使用了 MSCRM.SolutionName 标头,请在导出前确认已添加组件:
sol = client.records.list("solution",
filter="uniquename eq '<SOLUTION_NAME>'", 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")
幂等创建表
以编程方式创建表时(例如可能会重复运行的架构设置脚本),请采用先检查的模式——先查询 client.tables.get(),再创建。这样做明确且不会掩盖无关错误,还能根据表是新建还是复用来分支处理:
def ensure_table(client, schema_name, columns, solution, primary_column="prefix_Name", display_name=None):
existing = client.tables.get(schema_name)
if existing:
print(f"Reusing: {schema_name}")
return existing
info = client.tables.create(schema_name, columns, solution=solution,
primary_column=primary_column, display_name=display_name)
print(f"Created: {info['table_schema_name']}")
return info
备用键(Upsert 必需)
UpsertMultiple 要求设置备用键,以指定 Dataverse 应根据哪些列识别现有记录。请务必在架构设置时,在源系统 ID 列(prefix_Src*Id)上创建备用键,使每次导入都具有幂等性。
快速参考:
- SDK 调用:
client.tables.create_alternate_key(table, key_name, [columns], display_name=...)。复合键:传入多列。 - 使用先检查的模式,通过
client.tables.get_alternate_keys(table)跳过已存在的键——有关ensure_alternate_key辅助函数,请参阅references/alternate-keys.md。 - 索引创建是异步的——对于大型表,请轮询
client.tables.get_alternate_keys(table),直到status == "Active"后再使用。 - 限制:每个表最多 16 列/900 字节/10 个键;有效类型包括 Integer / Decimal / String / DateTime / Lookup / OptionSet。
有关 SDK 代码示例(单列键、复合键、幂等创建及状态检查)、Agent 选择列的决策规则(数据库源与 Excel/CSV),以及故障处理说明,请参阅 references/alternate-keys.md。
EntityDefinitions 筛选限制
startswith() 不支持用作 EntityDefinitions 的筛选条件。 此查询将返回 400 错误:
GET /api/data/v9.2/EntityDefinitions?$filter=startswith(LogicalName,'new_') # BROKEN
若要检索多个自定义表的元数据,请逐个查询每个表:
GET /api/data/v9.2/EntityDefinitions(LogicalName='new_projectbudget')?$select=LogicalName,EntitySetName
或者查询全部内容,再在客户端进行筛选:
GET /api/data/v9.2/EntityDefinitions?$select=LogicalName,EntitySetName
MCP 表创建说明
使用 MCP create_table 或 update_table 时:
- 列类型。 MCP 支持大多数类型(文本、数值、布尔值、日期时间、选项/多选、查找/客户、文件/图像);全局选项集、N:N、备用键、表单、视图需要使用 SDK/Web API。
- 超时/缓存延迟。 创建操作可能报告超时或缓存过期;重试或后续调用
update_table之前,请务必先执行describe('tables/{name}')(如果表已存在,则跳过创建)。 - 自引用查找(父项 → 同一张表)需在创建后通过
update_table添加。