[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M69AQP3WCSN3K3DCRD0E2PFF":3,"skill-same:01M69AQP3WCSN3K3DCRD0E2PFF":69},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":27,"linkOnly":28,"markdownContent":29,"markdownBody":30,"frontMatter":31,"refName":32,"commitSha":33,"skillDirectory":34,"forksCount":35,"repositoryHtmlUrl":36,"repositoryHomepageUrl":6,"ownerHtmlUrl":37,"ownerBlogUrl":38,"ownerDescription":39,"updatedAt":40,"install":41,"resources":54},"01M69AQP3WCSN3K3DCRD0E2PFF","dv-metadata",null,"Dataverse schema authoring and inspection — tables, columns, relationships, forms, and views. Use when the user wants to define, evolve, or inspect the data model — add a column, create a table, set up a lookup, customize a form, build a view, or list existing columns and relationships.","microsoft\u002FDataverse-skills","microsoft","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F6154722?v=4","01M698VYDXZ6JW5WH3CRV650R8","Microsoft","01M69AQ9SHMSQ8KC4EKV8DZW2Y",258,"MIT","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills\u002Fblob\u002Fmain\u002F.github\u002Fplugins\u002Fdataverse\u002Fskills\u002Fdv-metadata\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[24,25,26],"dv metadata","dataverse","customize","2026-11-10 10:12:30",false,"---\nname: dv-metadata\ndescription: Dataverse schema authoring and inspection — tables, columns, relationships, forms, and views. Use when the user wants to define, evolve, or inspect the data model — add a column, create a table, set up a lookup, customize a form, build a view, or list existing columns and relationships.\n---\n\n# Skill: Metadata — Making Changes\n\n**Before the first metadata change in a session:**\n1. **Confirm the target environment** with the user — see the Multi-Environment Rule in dv-overview.\n2. **Confirm the solution** — ask \"What solution should these components go into?\" If `SOLUTION_NAME` is in `.env`, confirm it. If no solution exists yet, **you MUST ask the user** for the solution name and publisher prefix before creating anything. The publisher prefix is **permanent** — it cannot be changed after components are created with it.\n\n**STOP and ask the user:**\n> \"What solution name and publisher prefix should I use? The prefix (e.g., `contoso`, `lit`, `soc`) is permanent on every table and column.\"\n\nThen query existing publishers and show them — the user may want to reuse one:\n\n```python\n# Publisher discovery + solution creation — use SDK (never raw Web API).\n# See dv-solution for the full publisher discovery flow.\npublishers = client.records.list(\"publisher\",\n    filter=\"customizationprefix ne 'none' and uniquename ne 'MicrosoftCorporation'\",\n    select=[\"publisherid\", \"uniquename\", \"friendlyname\", \"customizationprefix\"], top=10)\n# MANDATORY: Show existing publishers to user and ask which to use or create new\n```\n\nAfter user confirms, create using SDK:\n\n```python\npublisher_id = client.records.create(\"publisher\", {\n    \"uniquename\": \"\u003Cname>\", \"friendlyname\": \"\u003Cdisplay>\",\n    \"customizationprefix\": \"\u003Cprefix>\",  # from user input, NOT hardcoded\n    \"description\": \"\u003Cdesc>\",\n})\nsolution_id = client.records.create(\"solution\", {\n    \"uniquename\": \"\u003CSolutionName>\", \"friendlyname\": \"\u003CDisplay Name>\",\n    \"version\": \"1.0.0.0\",\n    \"publisherid@odata.bind\": f\"\u002Fpublishers({publisher_id})\",\n})\n```\n\nNever create tables or columns outside a solution.\n\n3. Pass `solution=\"\u003CUniqueName>\"` in every SDK call, or include `\"MSCRM.SolutionName\": \"\u003CUniqueName>\"` on every raw Web API call.\n\n## Skill boundaries\n\n| Need | Use instead |\n|---|---|\n| Create, update, or delete data records | **dv-data** |\n| Query or read records | **dv-query** |\n| Export or deploy solutions | **dv-solution** |\n| ERP schema | **dv-query**; [ERP routing](..\u002Fdv-overview\u002Freferences\u002Ferp-target.md) |\n\n---\n\n## How Changes Are Made: Environment-First\n\n**Do not write solution XML by hand to create new tables, columns, forms, or views.**\n\nThe environment validates metadata far more reliably than an agent editing XML. The correct workflow is:\n\n1. **Make the change in the environment** via the Dataverse MetadataService API (or `pac` commands where available)\n2. **Pull the change into the repo** via `pac solution export` + `pac solution unpack`\n3. **Commit the result**\n\nThe exported XML is generated by Dataverse itself and is always valid. Hand-written XML is fragile — a single incorrect attribute or missing element causes an import failure with an opaque error.\n\nThe only time you write files directly is when editing something that already exists in the repo (e.g., tweaking an existing view's columns or modifying a form layout you've already pulled).\n\n**CLI schema inspection:** `dataverse data describe --table account --include all --json` (uses LogicalName). API discovery: `dataverse api list --target dataverse`.\n\n---\n\n## Creating a Table\n\n**If creating multiple tables for a data import**, also see these sections later in this skill:\n- **Idempotent Table Creation** — check-first pattern for re-runnable scripts\n- **Alternate Keys** — required for upsert; create immediately after each table\n- **Metadata Propagation Delays and Lock Contention** — phased creation to avoid lock errors\n\n**Prefer the SDK for table creation** — use raw Web API only when you need full control over OwnershipType, HasActivities, or other advanced properties the SDK doesn't expose.\n\n**SDK approach (use this by default):**\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\u002Fskill\u002Fagent). Never include secrets or PII.\nclient = get_client(\"dv-metadata\")\n\ninfo = client.tables.create(\n    \"new_ProjectBudget\",\n    {\"new_Amount\": \"decimal\", \"new_Description\": \"string\"},\n    solution=\"MySolution\",\n    primary_column=\"new_Name\",\n    display_name=\"Project Budget\",  # human-readable name; plural auto-appends \"s\"\n)\nprint(f\"Created: {info['table_schema_name']}\")\n```\n\n**Web API fallback (ONLY when you need OwnershipType, HasActivities, or other properties the SDK doesn't expose):**\n\n```python\n# Helper for Label boilerplate\ndef label(text):\n    return {\"@odata.type\": \"Microsoft.Dynamics.CRM.Label\",\n            \"LocalizedLabels\": [{\"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\",\n                                  \"Label\": text, \"LanguageCode\": 1033}]}\n\nentity = {\n    \"@odata.type\": \"Microsoft.Dynamics.CRM.EntityMetadata\",\n    \"SchemaName\": \"new_ProjectBudget\",\n    \"DisplayName\": label(\"Project Budget\"),\n    \"DisplayCollectionName\": label(\"Project Budgets\"),\n    \"Description\": label(\"\"),\n    \"OwnershipType\": \"UserOwned\",\n    \"HasActivities\": False, \"HasNotes\": False, \"IsActivity\": False,\n    \"PrimaryNameAttribute\": \"new_name\",\n    \"Attributes\": [{\n        \"@odata.type\": \"Microsoft.Dynamics.CRM.StringAttributeMetadata\",\n        \"SchemaName\": \"new_name\",\n        \"DisplayName\": label(\"Name\"),\n        \"RequiredLevel\": {\"Value\": \"ApplicationRequired\"},\n        \"MaxLength\": 100, \"IsPrimaryName\": True,\n    }]\n}\n# POST to \u002Fapi\u002Fdata\u002Fv9.2\u002FEntityDefinitions with MSCRM.SolutionUniqueName header\n```\n\n---\n\n## Column Naming: Avoid `*Id` Suffix Collisions\n\n**Never name a regular column with an `Id` suffix** (e.g., `prefix_CountryId`). Dataverse auto-generates a navigation property with the `Id` suffix when you create a lookup — if a regular column with that name exists, lookup creation fails with a schema name collision.\n\n- WRONG: `prefix_DepartmentId` (int) — collides with auto-generated lookup\n- RIGHT: `prefix_SrcDepartmentId` or `prefix_DepartmentSourceId`\n\n---\n\n## Adding Columns\n\n**SDK approach (preferred):**\n\n```python\ncreated = client.tables.add_columns(\n    \"new_ProjectBudget\",\n    {\"new_Description\": \"string\", \"new_Amount\": \"decimal\", \"new_Active\": \"bool\"},\n)\nprint(created)  # ['new_Description', 'new_Amount', 'new_Active']\n```\n\nSupported type strings: `\"string\"` \u002F `\"text\"`, `\"int\"` \u002F `\"integer\"`, `\"decimal\"` \u002F `\"money\"`, `\"float\"` \u002F `\"double\"`, `\"datetime\"` \u002F `\"date\"`, `\"bool\"` \u002F `\"boolean\"`, `\"file\"`, and `Enum` subclasses (for local option sets).\n\n**Choice (picklist) column via SDK:**\n\n```python\nfrom enum import IntEnum\n\nclass BudgetStatus(IntEnum):\n    DRAFT = 100000000\n    APPROVED = 100000001\n    REJECTED = 100000002\n\ncreated = client.tables.add_columns(\n    \"new_ProjectBudget\",\n    {\"new_Status\": BudgetStatus},\n)\n```\n\n**Set constraints — pass a dict spec** instead of a bare type string (keys: `max_length`, `min_value`, `max_value`, `precision`, `format`, `required`, `display_name`); works on `create` and `add_columns`, no raw Web API:\n\n```python\nclient.tables.add_columns(\"new_ProjectBudget\", {\n    \"new_Amount\":  {\"type\": \"money\", \"min_value\": 0, \"max_value\": 1000000000, \"precision\": 2},\n    \"new_Comment\": {\"type\": \"memo\", \"max_length\": 2000, \"display_name\": \"Comment\"},\n})\n```\n\nChange an existing column in place with `update_column(table, col, spec)` or `update_columns(table, {col: spec})` — same keys; all specs are validated before any write.\n\n---\n\n## Lookup Columns and Relationships\n\n**SDK approach — simple lookup (preferred):**\n\n```python\nresult = client.tables.create_lookup_field(\n    referencing_table=\"new_projectbudget\",\n    lookup_field_name=\"new_AccountId\",\n    referenced_table=\"account\",\n    display_name=\"Account\",\n    solution=\"MySolution\",\n)\nprint(f\"Created lookup: {result.lookup_schema_name}\")\n```\n\n**SDK approach — full control over 1:N relationship:**\n\n```python\nfrom PowerPlatform.Dataverse.models.relationship import (\n    LookupAttributeMetadata,\n    OneToManyRelationshipMetadata,\n    CascadeConfiguration,\n)\nfrom PowerPlatform.Dataverse.models.labels import Label, LocalizedLabel\nfrom PowerPlatform.Dataverse.common.constants import CASCADE_BEHAVIOR_REMOVE_LINK\n\nlookup = LookupAttributeMetadata(\n    schema_name=\"new_AccountId\",\n    display_name=Label(localized_labels=[LocalizedLabel(label=\"Account\", language_code=1033)]),\n)\n\nrelationship = OneToManyRelationshipMetadata(\n    schema_name=\"account_new_projectbudget\",\n    referenced_entity=\"account\",\n    referencing_entity=\"new_projectbudget\",\n    referenced_attribute=\"accountid\",\n    cascade_configuration=CascadeConfiguration(delete=CASCADE_BEHAVIOR_REMOVE_LINK),\n)\n\nresult = client.tables.create_one_to_many_relationship(lookup, relationship, solution=\"MySolution\")\nprint(f\"Created: {result.relationship_schema_name}\")\n```\n\n**SDK approach — many-to-many relationship:**\n\n```python\nfrom PowerPlatform.Dataverse.models.relationship import ManyToManyRelationshipMetadata\n\nrelationship = ManyToManyRelationshipMetadata(\n    schema_name=\"new_ticket_knowledgebase\",\n    entity1_logical_name=\"new_ticket\",\n    entity2_logical_name=\"new_knowledgebase\",\n)\n\nresult = client.tables.create_many_to_many_relationship(relationship, solution=\"MySolution\")\nprint(f\"Created: {result.relationship_schema_name}\")\n```\n\n**Web API approach (fallback when SDK patterns don't suffice):**\n\n```python\nrelationship = {\n    \"@odata.type\": \"Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata\",\n    \"SchemaName\": \"account_new_projectbudget\",\n    \"ReferencedEntity\": \"account\",\n    \"ReferencingEntity\": \"new_projectbudget\",\n    \"Lookup\": {\n        \"@odata.type\": \"Microsoft.Dynamics.CRM.LookupAttributeMetadata\",\n        \"SchemaName\": \"new_AccountId\",\n        \"DisplayName\": {\"@odata.type\": \"Microsoft.Dynamics.CRM.Label\",\n                        \"LocalizedLabels\": [{\"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\",\n                                              \"Label\": \"Account\", \"LanguageCode\": 1033}]},\n        \"RequiredLevel\": {\"Value\": \"None\"}\n    }\n}\n# POST to \u002Fapi\u002Fdata\u002Fv9.2\u002FRelationshipDefinitions\n```\n\n**After creating a lookup — the @odata.bind navigation property:**\n\nWhen you create records that set this lookup, you need the **navigation property name** for `@odata.bind`. The navigation property name is case-sensitive and must match the entity's `$metadata` (usually the SchemaName of the lookup field, e.g., `new_AccountId`):\n\n| Navigation Property Name | `@odata.bind` key | Entity set |\n|---|---|---|\n| `new_AccountId` | `new_AccountId@odata.bind` | `\u002Faccounts(\u003Cguid>)` |\n| `new_ParentTicketId` | `new_ParentTicketId@odata.bind` | `\u002Fnew_tickets(\u003Cguid>)` |\n\n**Common mistake:** Using the logical name (lowercase) like `new_accountid@odata.bind` returns a 400 error. Navigation property names are case-sensitive and must match the entity's `$metadata`.\n\n---\n\n## Adding a Table to a Solution\n\nAfter creating a table via API, add it to your solution so it gets pulled on export:\n```\npac solution add-solution-component \\\n  --solutionUniqueName \u003CSOLUTION_NAME> \\\n  --component \u003CSchemaName> \\\n  --componentType 1 \\\n  --environment \u003Curl>\n```\nComponent type `1` = Entity (Table). See dv-solution for the full type code list.\n\nOr via Web API:\n```python\n# POST to \u002Fapi\u002Fdata\u002Fv9.2\u002FAddSolutionComponent\nbody = {\n    \"ComponentId\": \"\u003Centity-metadata-id>\",\n    \"ComponentType\": 1,       # 1 = Entity\n    \"SolutionUniqueName\": \"\u003CSOLUTION_NAME>\",\n    \"AddRequiredComponents\": True\n}\n```\n\n---\n\n## Forms and Views\n\n`systemform` (forms) and `savedquery` (views) are **ordinary entities** — create\u002Fread\u002Fmodify them with the **SDK's record CRUD** (no `urllib`). Only **publishing** (`PublishXml`, an unbound action) needs `dataverse api request`.\n\n**Quick reference:**\n- **Create form:** `client.records.create(\"systemform\", {...formxml..., \"type\": 2})` (`2`=Main, `7`=Quick Create, `6`=Quick View, `11`=Card).\n- **Modify form:** `records.list(\"systemform\", filter=...)` for a template → mutate `formxml` → `records.update(\"systemform\", id, {...})` → publish.\n- **Publish:** `dataverse api request` POST `PublishXml` — required for changes to take effect.\n- **Create view:** `client.records.create(\"savedquery\", {...fetchxml..., ...layoutxml...})` (`0`=standard, `1`=advanced find, `2`=associated, `4`=quick find).\n\nFull code, the template recipe, the `classid` table, and publish: [`references\u002Fforms-and-views.md`](references\u002Fforms-and-views.md).\n\nKey invariants:\n- All `id` attributes in form XML must be unique GUIDs (`str(uuid.uuid4()).upper()`).\n- Do not use `python -c` for GUID generation on Windows — write a `.py` file.\n- Forms must be published after every create or modify, otherwise changes are invisible to users.\n\n---\n\n## Business Rules\n\nCreate business rules in the Power Apps maker portal. They are too complex to write reliably as JSON\u002FXAML. After creation, export+unpack the solution and commit the result.\n\n---\n\n## Publisher Prefix\n\nAll custom schema names must use your solution's publisher prefix (e.g., `new_`, `contoso_`). Find yours:\n```\npac solution list --environment \u003Curl>\n```\nOr check `solutions\u002F\u003CSOLUTION_NAME>\u002FOther\u002FSolution.xml` after the first pull — look for `\u003CCustomizationPrefix>`.\n\n---\n\n## FormXml Pitfalls\n\n- **All `id` attributes must be valid GUIDs** in `{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}` format. Do not use strings like `\"general\"`.\n- **`labelid` is also a GUID** — not a human-readable string.\n- **Subgrid controls require a valid `\u003CViewId>`** — must be the GUID of an existing SavedQuery. Create the view first.\n- **Cell, section, tab, and control IDs must all be unique** across the entire form.\n- **Control `classid` values** — see the classid table above.\n\n**Tip:** Create forms in the maker portal and pull via `pac solution export` — use the pulled XML as a template for programmatic creation.\n\n---\n\n## After Creating Columns: Report Logical Names\n\nAfter creating columns (via Web API or MCP), **always report the actual logical names** to the user. Column names may be normalized or prefixed in ways the user doesn't expect. Summarize in a table:\n\n| Display Name | Logical Name | Type |\n|---|---|---|\n| Email | cr9ac_email | String |\n| Tier | cr9ac_tier | Picklist |\n| Customer | cr9ac_customerid | Lookup |\n\nThis prevents downstream failures when the user tries to insert data using incorrect column names.\n\n---\n\n## Common Web API Error Codes\n\n| Error Code | Meaning | Recovery |\n|---|---|---|\n| `0x80040216` | Transient metadata cache error. Column or table metadata not yet propagated. | Wait 3-5 seconds and retry. Usually succeeds on second attempt. |\n| `0x80048d19` | Invalid property in payload. A field name doesn't match any column on the table. | Check logical column names — use `EntityDefinitions(LogicalName='...')\u002FAttributes` to verify. |\n| `0x80040237` | Schema name already exists. | Verify the column\u002Ftable exists before creating a new one — it may have been created by a previous timed-out call. |\n| `0x8004431a` | Publisher prefix mismatch. | Ensure all schema names use the solution's publisher prefix. |\n| `0x80060891` | Metadata cache not ready after table creation. | Call `GET EntityDefinitions(LogicalName='...')` first to force cache refresh, then retry. |\n\nAlways translate error codes to plain English before presenting them to the user.\n\n---\n\n## Metadata Propagation Delays and Lock Contention\n\nAfter creating tables \u002F columns \u002F alternate keys, Dataverse runs internal metadata operations (index build, cache propagation) for 3–30 seconds. Submitting another metadata operation while these run causes lock-contention errors.\n\n**Mitigation — phased creation, not interleaved.** Create ALL tables → wait 15–30s → create ALL alternate keys → wait 15–30s → create ALL lookups. Do NOT interleave operations on the same table.\n\n**Symptoms** (any of these means propagation isn't done):\n- Picklist column creation fails with `0x80040216`\n- Lookup `@odata.bind` fails with \"Invalid property\"\n- `update_table` (MCP) fails with \"EntityId not found in MetadataCache\"\n- Lookup or alternate-key creation fails with \"another customization operation is running\"\n\nFor the `retry_metadata` helper that catches transient lock errors and the full phased-creation sequence, see [`references\u002Fmetadata-propagation.md`](references\u002Fmetadata-propagation.md).\n\n## Inspect Existing Schema\n\nBefore changing a model, inspect what exists. These read-only calls return raw\nmetadata dicts (PascalCase names) and are safe to run anytime.\n\n> Assumes `client` from the auth setup shown earlier in this skill (`from auth import get_client`).\n\n```python\n# typed=True returns type-specific fields (MaxLength, MinValue\u002FMaxValue) in one request -- no @odata.type cast, no urllib\nfor col in client.tables.list_columns(\"account\", typed=True):\n    print(col[\"LogicalName\"], col.get(\"MaxLength\"))\n# get_column(table, col, typed=True) reads one column's constraints\n\n# All relationships for one table (1:N, N:1, and N:N combined)\nrels = client.tables.list_table_relationships(\"account\")\nfor rel in rels:\n    print(f\"{rel['SchemaName']} -> {rel.get('@odata.type')}\")\n\n# All relationships in the environment (optionally filtered)\nall_rels = client.tables.list_relationships(select=[\"SchemaName\", \"ReferencedEntity\", \"ReferencingEntity\"])\n```\n\nFor SQL-queryable column discovery (virtual\u002Fcomputed columns excluded), `dv-query` also has\n`client.query.sql_columns(table)`.\n\n## Session Closing: Pull to Repo\n\n**After every metadata session, perform the pull-to-repo sequence** — see dv-overview \"After Any Change: Pull to Repo\" for the full export\u002Funpack\u002Fcommit commands.\n\nIf you used the `MSCRM.SolutionName` header during creation, verify components were added before exporting:\n```python\nsol = client.records.list(\"solution\",\n    filter=\"uniquename eq '\u003CSOLUTION_NAME>'\", 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\n---\n\n## Idempotent Table Creation\n\nWhen creating tables programmatically (e.g., a schema setup script that may be re-run), use a check-first pattern — query `client.tables.get()` before creating. This is explicit, avoids masking unrelated errors, and lets you branch logic based on whether the table was created or reused:\n\n```python\ndef ensure_table(client, schema_name, columns, solution, primary_column=\"prefix_Name\", display_name=None):\n    existing = client.tables.get(schema_name)\n    if existing:\n        print(f\"Reusing: {schema_name}\")\n        return existing\n    info = client.tables.create(schema_name, columns, solution=solution,\n                                primary_column=primary_column, display_name=display_name)\n    print(f\"Created: {info['table_schema_name']}\")\n    return info\n```\n\n---\n\n## Alternate Keys (Required for Upsert)\n\n`UpsertMultiple` requires an alternate key on the column(s) Dataverse should use to identify existing records. Always create alternate keys on source-system ID columns (`prefix_Src*Id`) at schema-setup time so every import is idempotent.\n\n**Quick reference:**\n- SDK call: `client.tables.create_alternate_key(table, key_name, [columns], display_name=...)`. Composite keys: pass multiple columns.\n- Use a check-first pattern with `client.tables.get_alternate_keys(table)` to skip keys that already exist — see `references\u002Falternate-keys.md` for the `ensure_alternate_key` helper.\n- Index creation is **async** — for large tables, poll `client.tables.get_alternate_keys(table)` until `status == \"Active\"` before using.\n- Constraints: max 16 columns \u002F 900 bytes \u002F 10 keys per table; valid types are Integer \u002F Decimal \u002F String \u002F DateTime \u002F Lookup \u002F OptionSet.\n\nFor SDK code samples (single + composite + idempotent + status-check), the agent decision rules for which column to pick (DB source vs Excel\u002FCSV), and the failure-handling notes, see [`references\u002Falternate-keys.md`](references\u002Falternate-keys.md).\n\n## EntityDefinitions Filter Limitation\n\n**`startswith()` is NOT supported as a filter on `EntityDefinitions`.** This query will return a 400 error:\n\n```\nGET \u002Fapi\u002Fdata\u002Fv9.2\u002FEntityDefinitions?$filter=startswith(LogicalName,'new_')  # BROKEN\n```\n\nTo retrieve metadata for multiple custom tables, query each table individually:\n```python\nGET \u002Fapi\u002Fdata\u002Fv9.2\u002FEntityDefinitions(LogicalName='new_projectbudget')?$select=LogicalName,EntitySetName\n```\n\nOr query all and filter client-side:\n```python\nGET \u002Fapi\u002Fdata\u002Fv9.2\u002FEntityDefinitions?$select=LogicalName,EntitySetName\n```\n\n---\n\n## MCP Table Creation Notes\n\nWhen using MCP `create_table` or `update_table`:\n\n- **Column types.** MCP handles most types (text, numeric, boolean, datetime, choice\u002Fmultiselect, lookup\u002Fcustomer, file\u002Fimage); global option sets, N:N, alternate keys, forms, views need SDK\u002FWeb API.\n- **Timeouts \u002F cache delays.** Creation may report a timeout or stale cache; always `describe('tables\u002F{name}')` before retrying or a follow-up `update_table` (if the table exists, skip creation).\n- **Self-referential lookups** (Parent → same table) are added via `update_table` after creation.\n","# Skill: Metadata — Making Changes\n\n**Before the first metadata change in a session:**\n1. **Confirm the target environment** with the user — see the Multi-Environment Rule in dv-overview.\n2. **Confirm the solution** — ask \"What solution should these components go into?\" If `SOLUTION_NAME` is in `.env`, confirm it. If no solution exists yet, **you MUST ask the user** for the solution name and publisher prefix before creating anything. The publisher prefix is **permanent** — it cannot be changed after components are created with it.\n\n**STOP and ask the user:**\n> \"What solution name and publisher prefix should I use? The prefix (e.g., `contoso`, `lit`, `soc`) is permanent on every table and column.\"\n\nThen query existing publishers and show them — the user may want to reuse one:\n\n```python\n# Publisher discovery + solution creation — use SDK (never raw Web API).\n# See dv-solution for the full publisher discovery flow.\npublishers = client.records.list(\"publisher\",\n    filter=\"customizationprefix ne 'none' and uniquename ne 'MicrosoftCorporation'\",\n    select=[\"publisherid\", \"uniquename\", \"friendlyname\", \"customizationprefix\"], top=10)\n# MANDATORY: Show existing publishers to user and ask which to use or create new\n```\n\nAfter user confirms, create using SDK:\n\n```python\npublisher_id = client.records.create(\"publisher\", {\n    \"uniquename\": \"\u003Cname>\", \"friendlyname\": \"\u003Cdisplay>\",\n    \"customizationprefix\": \"\u003Cprefix>\",  # from user input, NOT hardcoded\n    \"description\": \"\u003Cdesc>\",\n})\nsolution_id = client.records.create(\"solution\", {\n    \"uniquename\": \"\u003CSolutionName>\", \"friendlyname\": \"\u003CDisplay Name>\",\n    \"version\": \"1.0.0.0\",\n    \"publisherid@odata.bind\": f\"\u002Fpublishers({publisher_id})\",\n})\n```\n\nNever create tables or columns outside a solution.\n\n3. Pass `solution=\"\u003CUniqueName>\"` in every SDK call, or include `\"MSCRM.SolutionName\": \"\u003CUniqueName>\"` on every raw Web API call.\n\n## Skill boundaries\n\n| Need | Use instead |\n|---|---|\n| Create, update, or delete data records | **dv-data** |\n| Query or read records | **dv-query** |\n| Export or deploy solutions | **dv-solution** |\n| ERP schema | **dv-query**; [ERP routing](..\u002Fdv-overview\u002Freferences\u002Ferp-target.md) |\n\n---\n\n## How Changes Are Made: Environment-First\n\n**Do not write solution XML by hand to create new tables, columns, forms, or views.**\n\nThe environment validates metadata far more reliably than an agent editing XML. The correct workflow is:\n\n1. **Make the change in the environment** via the Dataverse MetadataService API (or `pac` commands where available)\n2. **Pull the change into the repo** via `pac solution export` + `pac solution unpack`\n3. **Commit the result**\n\nThe exported XML is generated by Dataverse itself and is always valid. Hand-written XML is fragile — a single incorrect attribute or missing element causes an import failure with an opaque error.\n\nThe only time you write files directly is when editing something that already exists in the repo (e.g., tweaking an existing view's columns or modifying a form layout you've already pulled).\n\n**CLI schema inspection:** `dataverse data describe --table account --include all --json` (uses LogicalName). API discovery: `dataverse api list --target dataverse`.\n\n---\n\n## Creating a Table\n\n**If creating multiple tables for a data import**, also see these sections later in this skill:\n- **Idempotent Table Creation** — check-first pattern for re-runnable scripts\n- **Alternate Keys** — required for upsert; create immediately after each table\n- **Metadata Propagation Delays and Lock Contention** — phased creation to avoid lock errors\n\n**Prefer the SDK for table creation** — use raw Web API only when you need full control over OwnershipType, HasActivities, or other advanced properties the SDK doesn't expose.\n\n**SDK approach (use this by default):**\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\u002Fskill\u002Fagent). Never include secrets or PII.\nclient = get_client(\"dv-metadata\")\n\ninfo = client.tables.create(\n    \"new_ProjectBudget\",\n    {\"new_Amount\": \"decimal\", \"new_Description\": \"string\"},\n    solution=\"MySolution\",\n    primary_column=\"new_Name\",\n    display_name=\"Project Budget\",  # human-readable name; plural auto-appends \"s\"\n)\nprint(f\"Created: {info['table_schema_name']}\")\n```\n\n**Web API fallback (ONLY when you need OwnershipType, HasActivities, or other properties the SDK doesn't expose):**\n\n```python\n# Helper for Label boilerplate\ndef label(text):\n    return {\"@odata.type\": \"Microsoft.Dynamics.CRM.Label\",\n            \"LocalizedLabels\": [{\"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\",\n                                  \"Label\": text, \"LanguageCode\": 1033}]}\n\nentity = {\n    \"@odata.type\": \"Microsoft.Dynamics.CRM.EntityMetadata\",\n    \"SchemaName\": \"new_ProjectBudget\",\n    \"DisplayName\": label(\"Project Budget\"),\n    \"DisplayCollectionName\": label(\"Project Budgets\"),\n    \"Description\": label(\"\"),\n    \"OwnershipType\": \"UserOwned\",\n    \"HasActivities\": False, \"HasNotes\": False, \"IsActivity\": False,\n    \"PrimaryNameAttribute\": \"new_name\",\n    \"Attributes\": [{\n        \"@odata.type\": \"Microsoft.Dynamics.CRM.StringAttributeMetadata\",\n        \"SchemaName\": \"new_name\",\n        \"DisplayName\": label(\"Name\"),\n        \"RequiredLevel\": {\"Value\": \"ApplicationRequired\"},\n        \"MaxLength\": 100, \"IsPrimaryName\": True,\n    }]\n}\n# POST to \u002Fapi\u002Fdata\u002Fv9.2\u002FEntityDefinitions with MSCRM.SolutionUniqueName header\n```\n\n---\n\n## Column Naming: Avoid `*Id` Suffix Collisions\n\n**Never name a regular column with an `Id` suffix** (e.g., `prefix_CountryId`). Dataverse auto-generates a navigation property with the `Id` suffix when you create a lookup — if a regular column with that name exists, lookup creation fails with a schema name collision.\n\n- WRONG: `prefix_DepartmentId` (int) — collides with auto-generated lookup\n- RIGHT: `prefix_SrcDepartmentId` or `prefix_DepartmentSourceId`\n\n---\n\n## Adding Columns\n\n**SDK approach (preferred):**\n\n```python\ncreated = client.tables.add_columns(\n    \"new_ProjectBudget\",\n    {\"new_Description\": \"string\", \"new_Amount\": \"decimal\", \"new_Active\": \"bool\"},\n)\nprint(created)  # ['new_Description', 'new_Amount', 'new_Active']\n```\n\nSupported type strings: `\"string\"` \u002F `\"text\"`, `\"int\"` \u002F `\"integer\"`, `\"decimal\"` \u002F `\"money\"`, `\"float\"` \u002F `\"double\"`, `\"datetime\"` \u002F `\"date\"`, `\"bool\"` \u002F `\"boolean\"`, `\"file\"`, and `Enum` subclasses (for local option sets).\n\n**Choice (picklist) column via SDK:**\n\n```python\nfrom enum import IntEnum\n\nclass BudgetStatus(IntEnum):\n    DRAFT = 100000000\n    APPROVED = 100000001\n    REJECTED = 100000002\n\ncreated = client.tables.add_columns(\n    \"new_ProjectBudget\",\n    {\"new_Status\": BudgetStatus},\n)\n```\n\n**Set constraints — pass a dict spec** instead of a bare type string (keys: `max_length`, `min_value`, `max_value`, `precision`, `format`, `required`, `display_name`); works on `create` and `add_columns`, no raw Web API:\n\n```python\nclient.tables.add_columns(\"new_ProjectBudget\", {\n    \"new_Amount\":  {\"type\": \"money\", \"min_value\": 0, \"max_value\": 1000000000, \"precision\": 2},\n    \"new_Comment\": {\"type\": \"memo\", \"max_length\": 2000, \"display_name\": \"Comment\"},\n})\n```\n\nChange an existing column in place with `update_column(table, col, spec)` or `update_columns(table, {col: spec})` — same keys; all specs are validated before any write.\n\n---\n\n## Lookup Columns and Relationships\n\n**SDK approach — simple lookup (preferred):**\n\n```python\nresult = client.tables.create_lookup_field(\n    referencing_table=\"new_projectbudget\",\n    lookup_field_name=\"new_AccountId\",\n    referenced_table=\"account\",\n    display_name=\"Account\",\n    solution=\"MySolution\",\n)\nprint(f\"Created lookup: {result.lookup_schema_name}\")\n```\n\n**SDK approach — full control over 1:N relationship:**\n\n```python\nfrom PowerPlatform.Dataverse.models.relationship import (\n    LookupAttributeMetadata,\n    OneToManyRelationshipMetadata,\n    CascadeConfiguration,\n)\nfrom PowerPlatform.Dataverse.models.labels import Label, LocalizedLabel\nfrom PowerPlatform.Dataverse.common.constants import CASCADE_BEHAVIOR_REMOVE_LINK\n\nlookup = LookupAttributeMetadata(\n    schema_name=\"new_AccountId\",\n    display_name=Label(localized_labels=[LocalizedLabel(label=\"Account\", language_code=1033)]),\n)\n\nrelationship = OneToManyRelationshipMetadata(\n    schema_name=\"account_new_projectbudget\",\n    referenced_entity=\"account\",\n    referencing_entity=\"new_projectbudget\",\n    referenced_attribute=\"accountid\",\n    cascade_configuration=CascadeConfiguration(delete=CASCADE_BEHAVIOR_REMOVE_LINK),\n)\n\nresult = client.tables.create_one_to_many_relationship(lookup, relationship, solution=\"MySolution\")\nprint(f\"Created: {result.relationship_schema_name}\")\n```\n\n**SDK approach — many-to-many relationship:**\n\n```python\nfrom PowerPlatform.Dataverse.models.relationship import ManyToManyRelationshipMetadata\n\nrelationship = ManyToManyRelationshipMetadata(\n    schema_name=\"new_ticket_knowledgebase\",\n    entity1_logical_name=\"new_ticket\",\n    entity2_logical_name=\"new_knowledgebase\",\n)\n\nresult = client.tables.create_many_to_many_relationship(relationship, solution=\"MySolution\")\nprint(f\"Created: {result.relationship_schema_name}\")\n```\n\n**Web API approach (fallback when SDK patterns don't suffice):**\n\n```python\nrelationship = {\n    \"@odata.type\": \"Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata\",\n    \"SchemaName\": \"account_new_projectbudget\",\n    \"ReferencedEntity\": \"account\",\n    \"ReferencingEntity\": \"new_projectbudget\",\n    \"Lookup\": {\n        \"@odata.type\": \"Microsoft.Dynamics.CRM.LookupAttributeMetadata\",\n        \"SchemaName\": \"new_AccountId\",\n        \"DisplayName\": {\"@odata.type\": \"Microsoft.Dynamics.CRM.Label\",\n                        \"LocalizedLabels\": [{\"@odata.type\": \"Microsoft.Dynamics.CRM.LocalizedLabel\",\n                                              \"Label\": \"Account\", \"LanguageCode\": 1033}]},\n        \"RequiredLevel\": {\"Value\": \"None\"}\n    }\n}\n# POST to \u002Fapi\u002Fdata\u002Fv9.2\u002FRelationshipDefinitions\n```\n\n**After creating a lookup — the @odata.bind navigation property:**\n\nWhen you create records that set this lookup, you need the **navigation property name** for `@odata.bind`. The navigation property name is case-sensitive and must match the entity's `$metadata` (usually the SchemaName of the lookup field, e.g., `new_AccountId`):\n\n| Navigation Property Name | `@odata.bind` key | Entity set |\n|---|---|---|\n| `new_AccountId` | `new_AccountId@odata.bind` | `\u002Faccounts(\u003Cguid>)` |\n| `new_ParentTicketId` | `new_ParentTicketId@odata.bind` | `\u002Fnew_tickets(\u003Cguid>)` |\n\n**Common mistake:** Using the logical name (lowercase) like `new_accountid@odata.bind` returns a 400 error. Navigation property names are case-sensitive and must match the entity's `$metadata`.\n\n---\n\n## Adding a Table to a Solution\n\nAfter creating a table via API, add it to your solution so it gets pulled on export:\n```\npac solution add-solution-component \\\n  --solutionUniqueName \u003CSOLUTION_NAME> \\\n  --component \u003CSchemaName> \\\n  --componentType 1 \\\n  --environment \u003Curl>\n```\nComponent type `1` = Entity (Table). See dv-solution for the full type code list.\n\nOr via Web API:\n```python\n# POST to \u002Fapi\u002Fdata\u002Fv9.2\u002FAddSolutionComponent\nbody = {\n    \"ComponentId\": \"\u003Centity-metadata-id>\",\n    \"ComponentType\": 1,       # 1 = Entity\n    \"SolutionUniqueName\": \"\u003CSOLUTION_NAME>\",\n    \"AddRequiredComponents\": True\n}\n```\n\n---\n\n## Forms and Views\n\n`systemform` (forms) and `savedquery` (views) are **ordinary entities** — create\u002Fread\u002Fmodify them with the **SDK's record CRUD** (no `urllib`). Only **publishing** (`PublishXml`, an unbound action) needs `dataverse api request`.\n\n**Quick reference:**\n- **Create form:** `client.records.create(\"systemform\", {...formxml..., \"type\": 2})` (`2`=Main, `7`=Quick Create, `6`=Quick View, `11`=Card).\n- **Modify form:** `records.list(\"systemform\", filter=...)` for a template → mutate `formxml` → `records.update(\"systemform\", id, {...})` → publish.\n- **Publish:** `dataverse api request` POST `PublishXml` — required for changes to take effect.\n- **Create view:** `client.records.create(\"savedquery\", {...fetchxml..., ...layoutxml...})` (`0`=standard, `1`=advanced find, `2`=associated, `4`=quick find).\n\nFull code, the template recipe, the `classid` table, and publish: [`references\u002Fforms-and-views.md`](references\u002Fforms-and-views.md).\n\nKey invariants:\n- All `id` attributes in form XML must be unique GUIDs (`str(uuid.uuid4()).upper()`).\n- Do not use `python -c` for GUID generation on Windows — write a `.py` file.\n- Forms must be published after every create or modify, otherwise changes are invisible to users.\n\n---\n\n## Business Rules\n\nCreate business rules in the Power Apps maker portal. They are too complex to write reliably as JSON\u002FXAML. After creation, export+unpack the solution and commit the result.\n\n---\n\n## Publisher Prefix\n\nAll custom schema names must use your solution's publisher prefix (e.g., `new_`, `contoso_`). Find yours:\n```\npac solution list --environment \u003Curl>\n```\nOr check `solutions\u002F\u003CSOLUTION_NAME>\u002FOther\u002FSolution.xml` after the first pull — look for `\u003CCustomizationPrefix>`.\n\n---\n\n## FormXml Pitfalls\n\n- **All `id` attributes must be valid GUIDs** in `{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}` format. Do not use strings like `\"general\"`.\n- **`labelid` is also a GUID** — not a human-readable string.\n- **Subgrid controls require a valid `\u003CViewId>`** — must be the GUID of an existing SavedQuery. Create the view first.\n- **Cell, section, tab, and control IDs must all be unique** across the entire form.\n- **Control `classid` values** — see the classid table above.\n\n**Tip:** Create forms in the maker portal and pull via `pac solution export` — use the pulled XML as a template for programmatic creation.\n\n---\n\n## After Creating Columns: Report Logical Names\n\nAfter creating columns (via Web API or MCP), **always report the actual logical names** to the user. Column names may be normalized or prefixed in ways the user doesn't expect. Summarize in a table:\n\n| Display Name | Logical Name | Type |\n|---|---|---|\n| Email | cr9ac_email | String |\n| Tier | cr9ac_tier | Picklist |\n| Customer | cr9ac_customerid | Lookup |\n\nThis prevents downstream failures when the user tries to insert data using incorrect column names.\n\n---\n\n## Common Web API Error Codes\n\n| Error Code | Meaning | Recovery |\n|---|---|---|\n| `0x80040216` | Transient metadata cache error. Column or table metadata not yet propagated. | Wait 3-5 seconds and retry. Usually succeeds on second attempt. |\n| `0x80048d19` | Invalid property in payload. A field name doesn't match any column on the table. | Check logical column names — use `EntityDefinitions(LogicalName='...')\u002FAttributes` to verify. |\n| `0x80040237` | Schema name already exists. | Verify the column\u002Ftable exists before creating a new one — it may have been created by a previous timed-out call. |\n| `0x8004431a` | Publisher prefix mismatch. | Ensure all schema names use the solution's publisher prefix. |\n| `0x80060891` | Metadata cache not ready after table creation. | Call `GET EntityDefinitions(LogicalName='...')` first to force cache refresh, then retry. |\n\nAlways translate error codes to plain English before presenting them to the user.\n\n---\n\n## Metadata Propagation Delays and Lock Contention\n\nAfter creating tables \u002F columns \u002F alternate keys, Dataverse runs internal metadata operations (index build, cache propagation) for 3–30 seconds. Submitting another metadata operation while these run causes lock-contention errors.\n\n**Mitigation — phased creation, not interleaved.** Create ALL tables → wait 15–30s → create ALL alternate keys → wait 15–30s → create ALL lookups. Do NOT interleave operations on the same table.\n\n**Symptoms** (any of these means propagation isn't done):\n- Picklist column creation fails with `0x80040216`\n- Lookup `@odata.bind` fails with \"Invalid property\"\n- `update_table` (MCP) fails with \"EntityId not found in MetadataCache\"\n- Lookup or alternate-key creation fails with \"another customization operation is running\"\n\nFor the `retry_metadata` helper that catches transient lock errors and the full phased-creation sequence, see [`references\u002Fmetadata-propagation.md`](references\u002Fmetadata-propagation.md).\n\n## Inspect Existing Schema\n\nBefore changing a model, inspect what exists. These read-only calls return raw\nmetadata dicts (PascalCase names) and are safe to run anytime.\n\n> Assumes `client` from the auth setup shown earlier in this skill (`from auth import get_client`).\n\n```python\n# typed=True returns type-specific fields (MaxLength, MinValue\u002FMaxValue) in one request -- no @odata.type cast, no urllib\nfor col in client.tables.list_columns(\"account\", typed=True):\n    print(col[\"LogicalName\"], col.get(\"MaxLength\"))\n# get_column(table, col, typed=True) reads one column's constraints\n\n# All relationships for one table (1:N, N:1, and N:N combined)\nrels = client.tables.list_table_relationships(\"account\")\nfor rel in rels:\n    print(f\"{rel['SchemaName']} -> {rel.get('@odata.type')}\")\n\n# All relationships in the environment (optionally filtered)\nall_rels = client.tables.list_relationships(select=[\"SchemaName\", \"ReferencedEntity\", \"ReferencingEntity\"])\n```\n\nFor SQL-queryable column discovery (virtual\u002Fcomputed columns excluded), `dv-query` also has\n`client.query.sql_columns(table)`.\n\n## Session Closing: Pull to Repo\n\n**After every metadata session, perform the pull-to-repo sequence** — see dv-overview \"After Any Change: Pull to Repo\" for the full export\u002Funpack\u002Fcommit commands.\n\nIf you used the `MSCRM.SolutionName` header during creation, verify components were added before exporting:\n```python\nsol = client.records.list(\"solution\",\n    filter=\"uniquename eq '\u003CSOLUTION_NAME>'\", 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\n---\n\n## Idempotent Table Creation\n\nWhen creating tables programmatically (e.g., a schema setup script that may be re-run), use a check-first pattern — query `client.tables.get()` before creating. This is explicit, avoids masking unrelated errors, and lets you branch logic based on whether the table was created or reused:\n\n```python\ndef ensure_table(client, schema_name, columns, solution, primary_column=\"prefix_Name\", display_name=None):\n    existing = client.tables.get(schema_name)\n    if existing:\n        print(f\"Reusing: {schema_name}\")\n        return existing\n    info = client.tables.create(schema_name, columns, solution=solution,\n                                primary_column=primary_column, display_name=display_name)\n    print(f\"Created: {info['table_schema_name']}\")\n    return info\n```\n\n---\n\n## Alternate Keys (Required for Upsert)\n\n`UpsertMultiple` requires an alternate key on the column(s) Dataverse should use to identify existing records. Always create alternate keys on source-system ID columns (`prefix_Src*Id`) at schema-setup time so every import is idempotent.\n\n**Quick reference:**\n- SDK call: `client.tables.create_alternate_key(table, key_name, [columns], display_name=...)`. Composite keys: pass multiple columns.\n- Use a check-first pattern with `client.tables.get_alternate_keys(table)` to skip keys that already exist — see `references\u002Falternate-keys.md` for the `ensure_alternate_key` helper.\n- Index creation is **async** — for large tables, poll `client.tables.get_alternate_keys(table)` until `status == \"Active\"` before using.\n- Constraints: max 16 columns \u002F 900 bytes \u002F 10 keys per table; valid types are Integer \u002F Decimal \u002F String \u002F DateTime \u002F Lookup \u002F OptionSet.\n\nFor SDK code samples (single + composite + idempotent + status-check), the agent decision rules for which column to pick (DB source vs Excel\u002FCSV), and the failure-handling notes, see [`references\u002Falternate-keys.md`](references\u002Falternate-keys.md).\n\n## EntityDefinitions Filter Limitation\n\n**`startswith()` is NOT supported as a filter on `EntityDefinitions`.** This query will return a 400 error:\n\n```\nGET \u002Fapi\u002Fdata\u002Fv9.2\u002FEntityDefinitions?$filter=startswith(LogicalName,'new_')  # BROKEN\n```\n\nTo retrieve metadata for multiple custom tables, query each table individually:\n```python\nGET \u002Fapi\u002Fdata\u002Fv9.2\u002FEntityDefinitions(LogicalName='new_projectbudget')?$select=LogicalName,EntitySetName\n```\n\nOr query all and filter client-side:\n```python\nGET \u002Fapi\u002Fdata\u002Fv9.2\u002FEntityDefinitions?$select=LogicalName,EntitySetName\n```\n\n---\n\n## MCP Table Creation Notes\n\nWhen using MCP `create_table` or `update_table`:\n\n- **Column types.** MCP handles most types (text, numeric, boolean, datetime, choice\u002Fmultiselect, lookup\u002Fcustomer, file\u002Fimage); global option sets, N:N, alternate keys, forms, views need SDK\u002FWeb API.\n- **Timeouts \u002F cache delays.** Creation may report a timeout or stale cache; always `describe('tables\u002F{name}')` before retrying or a follow-up `update_table` (if the table exists, skip creation).\n- **Self-referential lookups** (Parent → same table) are added via `update_table` after creation.\n",{"name":5,"description":7},"main","108370ef5d61358835af476c22c555dadefc55d0",".github\u002Fplugins\u002Fdataverse\u002Fskills\u002Fdv-metadata",65,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills","https:\u002F\u002Fgithub.com\u002Fmicrosoft","https:\u002F\u002Fopensource.microsoft.com","Open source projects and samples from Microsoft","2026-11-14 02:05:34",{"source":8,"skillName":5,"commands":42,"prompt":52,"zipAvailable":53},[43,46,49],{"tool":44,"command":45},"npx","npx skills add microsoft\u002FDataverse-skills --skill dv-metadata",{"tool":47,"command":48},"bunx","bunx skills add microsoft\u002FDataverse-skills --skill dv-metadata",{"tool":50,"command":51},"pnpm","pnpm dlx skills add microsoft\u002FDataverse-skills --skill dv-metadata","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills\u002Fblob\u002Fmain\u002F.github\u002Fplugins\u002Fdataverse\u002Fskills\u002Fdv-metadata\u002FSKILL.md",true,[55,61,65],{"path":56,"resourceType":57,"resourceTypeDesc":58,"sizeBytes":59,"sourceHtmlUrl":60},"references\u002Falternate-keys.md","REFERENCE","参考文档",4086,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills\u002Fblob\u002Fmain\u002F.github\u002Fplugins\u002Fdataverse\u002Fskills\u002Fdv-metadata\u002Freferences\u002Falternate-keys.md",{"path":62,"resourceType":57,"resourceTypeDesc":58,"sizeBytes":63,"sourceHtmlUrl":64},"references\u002Fforms-and-views.md",9192,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills\u002Fblob\u002Fmain\u002F.github\u002Fplugins\u002Fdataverse\u002Fskills\u002Fdv-metadata\u002Freferences\u002Fforms-and-views.md",{"path":66,"resourceType":57,"resourceTypeDesc":58,"sizeBytes":67,"sourceHtmlUrl":68},"references\u002Fmetadata-propagation.md",2279,"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills\u002Fblob\u002Fmain\u002F.github\u002Fplugins\u002Fdataverse\u002Fskills\u002Fdv-metadata\u002Freferences\u002Fmetadata-propagation.md",[70,109,138,150,160,191],{"skillNo":71,"name":72,"nameZh":6,"description":73,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":74,"categories":75,"keywords":88,"publishedAt":27},"01M6PJ8NCTZS7PZ5AVHXERS10A","erp-sql","Temporary Finance and Operations SQL access and bounded physical-table reads for unified development environments (UDEs) only, requiring the environment System administrator (sysAdmin) role. Use when the user explicitly requests direct ERP SQL access, a temporary SQL password, a sqlcmd connection, or a table read in an established ERP SQL workflow.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills\u002Fblob\u002Fmain\u002F.github\u002Fplugins\u002Fdataverse\u002Fskills\u002Ferp-sql\u002FSKILL.md",[76,79,80,85],{"categoryNo":77,"categoryName":78,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口",{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":81,"categoryName":82,"taxonomyType":83,"taxonomyTypeDesc":84},"01JBSEEDCAT000000000000033","数据分析师","PROFESSION","适用职业",{"categoryNo":86,"categoryName":87,"taxonomyType":83,"taxonomyTypeDesc":84},"01JBSEEDCAT000000000000040","财务",[89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108],"erp sql","sql","erp","system","sqlcmd","use","password","requests","operations","for","environments","direct","read","workflow","access","only","table","development","bounded","environment",{"skillNo":110,"name":111,"nameZh":6,"description":112,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":113,"categories":114,"keywords":120,"publishedAt":27},"01M69AR6XKM814KPH4PEEQE7R7","erp-xpp","X++ code development lifecycle for Finance and Operations — scaffold models, author classes, custom services\u002FAPIs, and data entities, install matching SDKs, compile deployable packages, deploy packages, synchronize databases, and verify deployed artifacts. Use when the user explicitly requests creating, building, compiling, packaging, deploying, DB-syncing, or verifying X++ customizations, or an end-to-end ERP code change.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills\u002Fblob\u002Fmain\u002F.github\u002Fplugins\u002Fdataverse\u002Fskills\u002Ferp-xpp\u002FSKILL.md",[115,118,119],{"categoryNo":116,"categoryName":117,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000011","运维与部署",{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":86,"categoryName":87,"taxonomyType":83,"taxonomyTypeDesc":84},[121,122,91,123,124,125,126,127,128,129,130,131,132,133,134,135,136,98,106,137],"xpp","erp xpp","development lifecycle","install","packages","sdks","code","end","packaging","db","databases","data","change","custom","lifecycle","compile","compiling",{"skillNo":139,"name":140,"nameZh":6,"description":141,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":142,"categories":143,"keywords":145,"publishedAt":27},"01M69AR3MEKCYBD8FEKK80KJ5B","dv-solution","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 \u002F test \u002F prod.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills\u002Fblob\u002Fmain\u002F.github\u002Fplugins\u002Fdataverse\u002Fskills\u002Fdv-solution\u002FSKILL.md",[144],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},[146,147,148,149,25],"dv solution","test","deploy","validate",{"skillNo":151,"name":152,"nameZh":6,"description":153,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":154,"categories":155,"keywords":157,"publishedAt":27},"01M69AR0SGRQFYSFT071ZAS19V","dv-security","Security-role assignment, user access, application users, business units, and admin self-elevation in Dataverse environments. Use when the user wants to give someone access, grant a role, become an admin, or add a service principal.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills\u002Fblob\u002Fmain\u002F.github\u002Fplugins\u002Fdataverse\u002Fskills\u002Fdv-security\u002FSKILL.md",[156],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},[158,159,25],"security","dv security",{"skillNo":161,"name":162,"nameZh":6,"description":163,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":164,"categories":165,"keywords":173,"publishedAt":27},"01M69AQX9TXX472D6Q59RYSMXY","dv-query","Bulk reads, multi-page iteration, and analytics over Dataverse data. Use when the user wants to read, list, filter, aggregate, group, join, or analyze records — including pandas DataFrame workflows and notebook exploration. Also covers ERP (Finance and Operations \u002F F&O) business-data reads and runtime entity, schema, or action discovery; load dv-overview first, then this skill.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills\u002Fblob\u002Fmain\u002F.github\u002Fplugins\u002Fdataverse\u002Fskills\u002Fdv-query\u002FSKILL.md",[166,167,170,171,172],{"categoryNo":77,"categoryName":78,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":168,"categoryName":169,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000013","分析与可视化",{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":81,"categoryName":82,"taxonomyType":83,"taxonomyTypeDesc":84},{"categoryNo":86,"categoryName":87,"taxonomyType":83,"taxonomyTypeDesc":84},[174,175,176,177,178,179,180,181,94,182,183,101,184,185,186,187,188,189,190,25],"query","dv query","analytics","workflows","list","pandas","bulk","user","records","discovery","dataframe","analyze","filter","join","notebook","iteration","exploration",{"skillNo":192,"name":193,"nameZh":6,"description":194,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":195,"categories":196,"keywords":199,"publishedAt":27},"01M69AQSP7EV1XGQAA69Y9MJ8Y","dv-overview","Foundational cross-cutting context for Dataverse \u002F Power Platform work — scope and the skill map, the tool-capability reference, the safety rules, and the safe change lifecycle. Use when the user mentions Dataverse, Dynamics 365, Power Platform, CRM, or ERP; load this first for orientation. Specialist skills self-route via their own frontmatter triggers. Finance and Operations \u002F F&O business-data requests also require this skill first, then dv-query for reads.","https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002FDataverse-skills\u002Fblob\u002Fmain\u002F.github\u002Fplugins\u002Fdataverse\u002Fskills\u002Fdv-overview\u002FSKILL.md",[197,198],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":86,"categoryName":87,"taxonomyType":83,"taxonomyTypeDesc":84},[200,201,202,91,203,204,205,132,206,207,208,209,174,210,211,212,213,25,214,133],"dv overview","overview","route","capability","context","frontmatter","Safety","crm","work","self","map","skill map","scope","power","safe"]