dataverse-create-schema

Tier 1 Component Skill - Creates or updates Dataverse schema components programmatically using the Dataverse Web API and PowerShell. This skill provides patterns for:

SKILL.md

GitHub

dataverse-create-schema

Last Reviewed: 2026-05-17 Reviewed By: ai-procedure-quality-r1 (Phase 2b Wave 2d — fixed Reference Scripts section per user decision row 12 [lighter rewrite]: pointed at REAL existing scripts; dropped 3 broken refs to non-existent projects/ai-node-playbook-builder/scripts/*; kept inline PowerShell examples per dereferencing-reliability concern; marked leave-alone-justified on body length) Exemplar rationale: scripts/Deploy-ChartDefinitionEntity.ps1 is a real, complete pattern script showing entity creation with custom fields — verifiably exercisable. Justified length (777 lines): heavy PowerShell example density is the value proposition. An agent generating schema-creation scripts needs the patterns inline, not gestured-at. Splitting risks the dereferencing-reliability concern.

Purpose

Tier 1 Component Skill - Creates or updates Dataverse schema components programmatically using the Dataverse Web API and PowerShell. This skill provides patterns for:

  • Creating global option sets (choice columns)
  • Creating new entities (tables)
  • Adding attributes (columns) to entities
  • Creating lookup relationships (1:N)
  • Creating many-to-many relationships (N:N)
  • Extending existing entities with new fields

Why Web API Instead of PAC CLI?

  • PAC CLI (v1.46+) doesn't have direct pac table create or pac column create commands
  • Web API provides full control over all metadata properties
  • Idempotent scripts can safely re-run without errors
  • Supports all attribute types and relationship configurations

When to Use

  • User says "create Dataverse entity", "add column to table", "create relationship"
  • Task has tags: dataverse, schema, entity, table
  • Need to create/modify Dataverse schema programmatically
  • Deploying schema changes to multiple environments
  • Schema definition exists in design docs or POML task files

Pre-Validation via MCP (RECOMMENDED)

Before writing any PowerShell scripts, use MCP tools to inspect the current schema state. This prevents creating entities/attributes that already exist and catches naming conflicts early.

STEP 0: Schema Discovery (before any script generation)

1. LIST existing tables:
   → mcp__dataverse__list_tables()
   → Filter for sprk_* tables relevant to this task

2. DESCRIBE target entities:
   → mcp__dataverse__describe_table(tablename) for each entity
   → Note existing columns, types, lookups, option sets

3. COMPARE against task requirements:
   → Which entities already exist? (skip creation)
   → Which attributes already exist? (skip addition)
   → Which attributes need to be added? (generate script)
   → Any type mismatches? (flag for human review)

4. GENERATE PowerShell script with only the DELTA:
   → Skip existing entities (Test-EntityExists becomes redundant for known items)
   → Skip existing attributes (Test-AttributeExists becomes redundant for known items)
   → Focus script on net-new schema components only

Why MCP first? MCP describe_table returns a complete T-SQL schema in one call. The PowerShell Test-EntityExists / Test-AttributeExists functions require one API call per check. For a task creating 5 entities with 20 fields each, MCP pre-validation saves ~100 API calls.


Prerequisites

1. Azure CLI Authentication

Required for obtaining Dataverse access tokens:

# Login to Azure (if not already)
az login

# Verify account
az account show

2. PAC CLI Authentication (for verification)

# Authenticate to target environment
pac auth create --environment https://spaarkedev1.crm.dynamics.com

# Verify connection
pac auth list

3. Target Environment URL

Standard environments:

Environment URL
Dev https://spaarkedev1.crm.dynamics.com

Core Patterns

Authentication and Headers

# Get OAuth token using Azure CLI
$Environment = "spaarkedev1.crm.dynamics.com"
$token = (az account get-access-token --resource "https://$Environment" --query accessToken -o tsv)

# Standard headers for Dataverse Web API
$headers = @{
    "Authorization" = "Bearer $token"
    "OData-MaxVersion" = "4.0"
    "OData-Version" = "4.0"
    "Content-Type" = "application/json"
    "Accept" = "application/json"
    "Prefer" = "return=representation"
}

$BaseUrl = "https://$Environment/api/data/v9.2"

API Helper Function

function Invoke-DataverseApi {
    param(
        [string]$Token,
        [string]$BaseUrl,
        [string]$Endpoint,
        [string]$Method = "GET",
        [object]$Body = $null
    )

    $uri = "$BaseUrl/$Endpoint"
    $headers = @{
        "Authorization" = "Bearer $Token"
        "OData-MaxVersion" = "4.0"
        "OData-Version" = "4.0"
        "Content-Type" = "application/json"
        "Accept" = "application/json"
    }

    $params = @{
        Uri = $uri
        Headers = $headers
        Method = $Method
    }

    if ($Body) {
        $params.Body = ($Body | ConvertTo-Json -Depth 20 -Compress)
    }

    return Invoke-RestMethod @params
}

Label Helper

function New-Label {
    param([string]$Text)
    return @{
        "@odata.type" = "Microsoft.Dynamics.CRM.Label"
        "LocalizedLabels" = @(
            @{
                "@odata.type" = "Microsoft.Dynamics.CRM.LocalizedLabel"
                "Label" = $Text
                "LanguageCode" = 1033  # English
            }
        )
    }
}

Attribute Type Definitions

String Attribute

function New-StringAttribute {
    param(
        [string]$SchemaName,
        [string]$DisplayName,
        [string]$Description,
        [int]$MaxLength = 200,
        [bool]$Required = $false
    )
    return @{
        "@odata.type" = "Microsoft.Dynamics.CRM.StringAttributeMetadata"
        "SchemaName" = $SchemaName
        "RequiredLevel" = @{ "Value" = if ($Required) { "ApplicationRequired" } else { "None" } }
        "MaxLength" = $MaxLength
        "DisplayName" = New-Label -Text $DisplayName
        "Description" = New-Label -Text $Description
    }
}

Memo (Multiline Text) Attribute

function New-MemoAttribute {
    param(
        [string]$SchemaName,
        [string]$DisplayName,
        [string]$Description,
        [int]$MaxLength = 100000
    )
    return @{
        "@odata.type" = "Microsoft.Dynamics.CRM.MemoAttributeMetadata"
        "SchemaName" = $SchemaName
        "RequiredLevel" = @{ "Value" = "None" }
        "MaxLength" = $MaxLength
        "DisplayName" = New-Label -Text $DisplayName
        "Description" = New-Label -Text $Description
    }
}

Integer Attribute

function New-IntegerAttribute {
    param(
        [string]$SchemaName,
        [string]$DisplayName,
        [string]$Description,
        [bool]$Required = $false,
        [int]$MinValue = -2147483648,
        [int]$MaxValue = 2147483647
    )
    return @{
        "@odata.type" = "Microsoft.Dynamics.CRM.IntegerAttributeMetadata"
        "SchemaName" = $SchemaName
        "RequiredLevel" = @{ "Value" = if ($Required) { "ApplicationRequired" } else { "None" } }
        "DisplayName" = New-Label -Text $DisplayName
        "Description" = New-Label -Text $Description
        "MinValue" = $MinValue
        "MaxValue" = $MaxValue
    }
}

Boolean Attribute

Note: Boolean attributes require an OptionSet definition with TrueOption/FalseOption.

function New-BooleanAttribute {
    param(
        [string]$SchemaName,
        [string]$DisplayName,
        [string]$Description
    )
    return @{
        "@odata.type" = "Microsoft.Dynamics.CRM.BooleanAttributeMetadata"
        "SchemaName" = $SchemaName
        "RequiredLevel" = @{ "Value" = "None" }
        "DisplayName" = New-Label -Text $DisplayName
        "Description" = New-Label -Text $Description
        "OptionSet" = @{
            "TrueOption" = @{
                "Value" = 1
                "Label" = New-Label -Text "Yes"
            }
            "FalseOption" = @{
                "Value" = 0
                "Label" = New-Label -Text "No"
            }
        }
    }
}

Picklist (Choice) Attribute with Global Option Set

function New-PicklistAttribute {
    param(
        [string]$SchemaName,
        [string]$DisplayName,
        [string]$Description,
        [string]$GlobalOptionSetName,
        [bool]$Required = $false
    )
    return @{
        "@odata.type" = "Microsoft.Dynamics.CRM.PicklistAttributeMetadata"
        "SchemaName" = $SchemaName
        "RequiredLevel" = @{ "Value" = if ($Required) { "ApplicationRequired" } else { "None" } }
        "DisplayName" = New-Label -Text $DisplayName
        "Description" = New-Label -Text $Description
        "GlobalOptionSet@odata.bind" = "/GlobalOptionSetDefinitions(Name='$GlobalOptionSetName')"
    }
}

DateTime Attribute

function New-DateTimeAttribute {
    param(
        [string]$SchemaName,
        [string]$DisplayName,
        [string]$Description
    )
    return @{
        "@odata.type" = "Microsoft.Dynamics.CRM.DateTimeAttributeMetadata"
        "SchemaName" = $SchemaName
        "RequiredLevel" = @{ "Value" = "None" }
        "DisplayName" = New-Label -Text $DisplayName
        "Description" = New-Label -Text $Description
        "Format" = "DateAndTime"
        "DateTimeBehavior" = @{ "Value" = "UserLocal" }
    }
}

Entity Operations

Create New Entity

function New-Entity {
    param(
        [string]$Token,
        [string]$BaseUrl,
        [string]$SchemaName,
        [string]$DisplayName,
        [string]$PluralName,
        [string]$Description,
        [bool]$IsAutoNumber = $false
    )

    $entityDef = @{
        "@odata.type" = "Microsoft.Dynamics.CRM.EntityMetadata"
        "SchemaName" = $SchemaName
        "DisplayName" = New-Label -Text $DisplayName
        "DisplayCollectionName" = New-Label -Text $PluralName
        "Description" = New-Label -Text $Description
        "OwnershipType" = "OrganizationOwned"
        "IsActivity" = $false
        "HasNotes" = $false
        "HasActivities" = $false
        "PrimaryNameAttribute" = "sprk_name"
        "Attributes" = @(
            @{
                "@odata.type" = "Microsoft.Dynamics.CRM.StringAttributeMetadata"
                "SchemaName" = "sprk_name"
                "RequiredLevel" = @{ "Value" = "ApplicationRequired" }
                "MaxLength" = 200
                "DisplayName" = New-Label -Text "Name"
                "Description" = New-Label -Text "Primary name field"
                "IsPrimaryName" = $true
                "AutoNumberFormat" = if ($IsAutoNumber) { "{SEQNUM:6}" } else { $null }
            }
        )
    }

    Invoke-DataverseApi -Token $Token -BaseUrl $BaseUrl -Endpoint "EntityDefinitions" -Method "POST" -Body $entityDef
    Write-Host "  Created entity: $SchemaName" -ForegroundColor Green
}

Add Attribute to Existing Entity

function Add-EntityAttribute {
    param(
        [string]$Token,
        [string]$BaseUrl,
        [string]$EntityLogicalName,
        [hashtable]$AttributeDef
    )

    $endpoint = "EntityDefinitions(LogicalName='$EntityLogicalName')/Attributes"
    Invoke-DataverseApi -Token $Token -BaseUrl $BaseUrl -Endpoint $endpoint -Method "POST" -Body $AttributeDef
    Write-Host "    Created: $($AttributeDef.SchemaName)" -ForegroundColor Green
}

Check If Entity/Attribute Exists

Preferred: Use MCP tools (faster, no token management needed):

# Check entity existence — one call returns full schema
mcp__dataverse__describe_table(tablename="sprk_myentity")
→ If it returns schema: entity exists
→ If it errors: entity does not exist

# Check attribute existence — parse describe_table output
mcp__dataverse__describe_table(tablename="sprk_myentity")
→ Look for attribute name in the T-SQL column list

Fallback: PowerShell (for use inside deployment scripts where MCP is not available):

function Test-EntityExists {
    param([string]$Token, [string]$BaseUrl, [string]$LogicalName)
    try {
        Invoke-DataverseApi -Token $Token -BaseUrl $BaseUrl -Endpoint "EntityDefinitions(LogicalName='$LogicalName')" -Method "GET" | Out-Null
        return $true
    } catch { return $false }
}

function Test-AttributeExists {
    param([string]$Token, [string]$BaseUrl, [string]$EntityLogicalName, [string]$AttributeLogicalName)
    try {
        Invoke-DataverseApi -Token $Token -BaseUrl $BaseUrl -Endpoint "EntityDefinitions(LogicalName='$EntityLogicalName')/Attributes(LogicalName='$AttributeLogicalName')" -Method "GET" | Out-Null
        return $true
    } catch { return $false }
}

Relationship Operations

Create Lookup (1:N) Relationship

CRITICAL: Lookup attributes CANNOT be created directly via AttributeMetadata. You MUST create them via RelationshipDefinitions.

function New-OneToManyRelationship {
    param(
        [string]$Token,
        [string]$BaseUrl,
        [string]$ReferencedEntity,      # Parent entity (1 side)
        [string]$ReferencingEntity,     # Child entity (N side)
        [string]$LookupSchemaName,      # e.g., "sprk_parentid"
        [string]$LookupDisplayName,
        [string]$LookupDescription,
        [bool]$Required = $false,
        [string]$DeleteBehavior = "RemoveLink"  # RemoveLink, Restrict, Cascade
    )

    $relationshipSchemaName = "sprk_$($ReferencedEntity)_$($ReferencingEntity)_$($LookupSchemaName -replace 'sprk_', '')"

    $relationshipDef = @{
        "@odata.type" = "Microsoft.Dynamics.CRM.OneToManyRelationshipMetadata"
        "SchemaName" = $relationshipSchemaName
        "ReferencedEntity" = $ReferencedEntity
        "ReferencingEntity" = $ReferencingEntity
        "CascadeConfiguration" = @{
            "Assign" = "NoCascade"
            "Delete" = $DeleteBehavior
            "Merge" = "NoCascade"
            "Reparent" = "NoCascade"
            "Share" = "NoCascade"
            "Unshare" = "NoCascade"
        }
        "Lookup" = @{
            "SchemaName" = $LookupSchemaName
            "DisplayName" = New-Label -Text $LookupDisplayName
            "Description" = New-Label -Text $LookupDescription
            "RequiredLevel" = @{ "Value" = if ($Required) { "ApplicationRequired" } else { "None" } }
        }
    }

    Invoke-DataverseApi -Token $Token -BaseUrl $BaseUrl -Endpoint "RelationshipDefinitions" -Method "POST" -Body $relationshipDef
    Write-Host "    Created lookup: $LookupSchemaName" -ForegroundColor Green
}

Delete Behavior Options

Behavior Description
Cascade Delete child records when parent is deleted
Restrict Prevent parent deletion if children exist
RemoveLink Clear the lookup value on children (default)

Create Many-to-Many (N:N) Relationship

function New-ManyToManyRelationship {
    param(
        [string]$Token,
        [string]$BaseUrl,
        [string]$Entity1LogicalName,
        [string]$Entity2LogicalName,
        [string]$RelationshipSchemaName  # e.g., "sprk_entity1_entity2"
    )

    $relationshipDef = @{
        "@odata.type" = "Microsoft.Dynamics.CRM.ManyToManyRelationshipMetadata"
        "SchemaName" = $RelationshipSchemaName
        "Entity1LogicalName" = $Entity1LogicalName
        "Entity2LogicalName" = $Entity2LogicalName
        "Entity1AssociatedMenuConfiguration" = @{
            "Behavior" = "UseCollectionName"
            "Group" = "Details"
            "Order" = 10000
        }
        "Entity2AssociatedMenuConfiguration" = @{
            "Behavior" = "UseCollectionName"
            "Group" = "Details"
            "Order" = 10000
        }
        "IntersectEntityName" = $RelationshipSchemaName
    }

    Invoke-DataverseApi -Token $Token -BaseUrl $BaseUrl -Endpoint "RelationshipDefinitions" -Method "POST" -Body $relationshipDef
    Write-Host "  Created N:N: $RelationshipSchemaName" -ForegroundColor Green
}

Global Option Set Operations

Create Global Option Set

function New-GlobalOptionSet {
    param(
        [string]$Token,
        [string]$BaseUrl,
        [string]$Name,
        [string]$DisplayName,
        [string]$Description,
        [hashtable[]]$Options  # @{ Value = 0; Label = "Option1" }, ...
    )

    $optionSetDef = @{
        "@odata.type" = "Microsoft.Dynamics.CRM.OptionSetMetadata"
        "Name" = $Name
        "DisplayName" = New-Label -Text $DisplayName
        "Description" = New-Label -Text $Description
        "IsGlobal" = $true
        "OptionSetType" = "Picklist"
        "Options" = @(
            $Options | ForEach-Object {
                @{
                    "Value" = $_.Value
                    "Label" = New-Label -Text $_.Label
                }
            }
        )
    }

    Invoke-DataverseApi -Token $Token -BaseUrl $BaseUrl -Endpoint "GlobalOptionSetDefinitions" -Method "POST" -Body $optionSetDef
    Write-Host "  Created option set: $Name" -ForegroundColor Green
}

# Example usage:
$statusOptions = @(
    @{ Value = 0; Label = "Pending" },
    @{ Value = 1; Label = "Running" },
    @{ Value = 2; Label = "Completed" },
    @{ Value = 3; Label = "Failed" }
)
New-GlobalOptionSet -Token $token -BaseUrl $baseUrl -Name "sprk_status" -DisplayName "Status" -Description "Execution status" -Options $statusOptions

Get Global Option Set (for referencing in picklist attributes)

function Get-GlobalOptionSet {
    param([string]$Token, [string]$BaseUrl, [string]$Name)
    return Invoke-DataverseApi -Token $Token -BaseUrl $BaseUrl -Endpoint "GlobalOptionSetDefinitions(Name='$Name')" -Method "GET"
}

Publishing Customizations

After creating schema components, publish to make them available:

function Publish-Customizations {
    param(
        [string]$Token,
        [string]$BaseUrl,
        [string[]]$EntityLogicalNames  # Optional - specific entities only
    )

    if ($EntityLogicalNames -and $EntityLogicalNames.Count -gt 0) {
        $entityXml = ($EntityLogicalNames | ForEach-Object { "<entity>$_</entity>" }) -join ""
        $publishXml = @{
            "ParameterXml" = "<importexportxml><entities>$entityXml</entities></importexportxml>"
        }
    } else {
        $publishXml = @{
            "ParameterXml" = "<importexportxml><entities/></importexportxml>"
        }
    }

    Invoke-DataverseApi -Token $Token -BaseUrl $BaseUrl -Endpoint "PublishXml" -Method "POST" -Body $publishXml
    Write-Host "Customizations published" -ForegroundColor Green
}

Post-Deployment Verification via MCP

After running schema deployment scripts and publishing customizations, verify the schema was applied correctly:

VERIFY each entity created/modified:
  mcp__dataverse__describe_table(tablename="sprk_myentity")

CHECK:
  - All expected columns present with correct types
  - Lookup relationships point to correct related tables
  - Option set columns have correct valid options
  - Required fields marked correctly

REPORT:
  "✅ Schema verified via MCP:
   - sprk_myentity: 12 columns (expected 12) ✓
   - sprk_myentity.sprk_priority: PICKLIST with 4 options ✓
   - sprk_myentity.sprk_assignedto: LOOKUP → systemuser ✓"

This catches silent deployment failures where a script ran without errors but a field wasn't created (e.g., due to name collision or type mismatch).


Script Execution Order

When creating schema, follow this order to avoid dependency issues:

Phase 1: Global Option Sets
    ↓ (Option sets must exist before picklist attributes)
Phase 2: Lookup Reference Entities (small lookup tables)
    ↓ (Reference entities must exist before lookups to them)
Phase 3: Extend Existing Entities (add fields to existing tables)
    ↓ (Existing entities get new fields)
Phase 4: Create New Entities with Attributes and Relationships
    ↓ (New entities created with all components)
Phase 5: Create N:N Relationships
    ↓ (Both entities must exist first)
Phase 6: Publish Customizations

Reference Scripts

Updated 2026-05-17: This section was rewritten by ai-procedure-quality-r1 Wave 2d. Earlier versions referenced 3 scripts under projects/ai-node-playbook-builder/scripts/ — that directory does NOT exist in the current repo (archived/never-existed). The scripts below are REAL and EXIST in the repo.

These real scripts in the repository demonstrate the patterns this skill prescribes:

Multi-Entity Schema Deployment (5-phase pattern)

scripts/Deploy-ReportingSchema.ps1

  • Multi-entity deployment pattern (creates multiple related entities + relationships)
  • Idempotent (checks for existing items before creating)
  • Demonstrates option-set creation before entity creation (Phase 1 → Phase 4)

scripts/Deploy-NotificationPlaybooks.ps1

  • Similar multi-entity pattern with notification-specific entities

Single Entity + Custom Fields

scripts/Deploy-ChartDefinitionEntity.ps1 (canonical exemplar)

  • Original entity-with-custom-fields pattern
  • Shows entity creation, attribute additions, publish-customizations
  • Best starting point for "I need to create one new entity"

scripts/Create-RegistrationRequestSchema.ps1

  • Complete schema for a single business entity (sprk_demoregistrationrequest)
  • Includes attribute creation + publish

Add Attributes to Existing Entities

scripts/Create-PlaybookTriggerFields.ps1

  • Pattern for adding new attributes to existing entities
  • Useful for incremental schema updates without recreating the entity

Bulk Trigger Metadata

scripts/Seed-PlaybookTriggerMetadata.ps1

  • Companion script that populates trigger records after schema is in place
  • Shows the schema-then-data sequencing pattern

Common Errors and Solutions

Error Cause Solution
LookupAttributeMetadata cannot be created through the SDK Tried to create lookup via Attributes endpoint Use RelationshipDefinitions endpoint with OneToManyRelationshipMetadata
DefaultValue property does not exist DefaultValue not valid for some attribute types Remove DefaultValue from IntegerAttributeMetadata
An unexpected error occurred Transient API error or malformed request Re-run script (idempotent design), check JSON structure
Entity with name X already exists Entity already created Add existence check before creation
GlobalOptionSet not found Option set referenced before creation Create global option sets in Phase 1

Verification

After deployment, verify schema:

# Verify entity exists
$entity = Invoke-DataverseApi -Token $token -BaseUrl $baseUrl `
    -Endpoint "EntityDefinitions(LogicalName='sprk_myentity')?`$select=LogicalName"

# Verify attributes
$attrs = Invoke-DataverseApi -Token $token -BaseUrl $baseUrl `
    -Endpoint "EntityDefinitions(LogicalName='sprk_myentity')?`$expand=Attributes(`$select=LogicalName,AttributeType)"

$attrs.Attributes | Where-Object { $_.LogicalName -like "sprk_*" } |
    ForEach-Object { "$($_.LogicalName) ($($_.AttributeType))" }

# Verify relationships
$rels = Invoke-DataverseApi -Token $token -BaseUrl $baseUrl `
    -Endpoint "EntityDefinitions(LogicalName='sprk_myentity')/ManyToOneRelationships?`$select=SchemaName,ReferencedEntity"

# Verify N:N relationships
$nn = Invoke-DataverseApi -Token $token -BaseUrl $baseUrl `
    -Endpoint "EntityDefinitions(LogicalName='sprk_myentity')/ManyToManyRelationships?`$select=SchemaName"

Integration with Other Skills

Skill Relationship
dataverse-deploy Deploy solutions after schema creation
task-execute May invoke this skill for tasks with dataverse, schema tags
adr-aware ADR-022 requires unmanaged solutions

ADR Compliance

  • ADR-022: All schema changes must use unmanaged solutions only
  • Schema created via Web API is automatically unmanaged
  • Never deploy managed solutions to dev environments

Example: Complete Entity Creation

# Create entity with all attribute types
$token = (az account get-access-token --resource "https://spaarkedev1.crm.dynamics.com" --query accessToken -o tsv)
$baseUrl = "https://spaarkedev1.crm.dynamics.com/api/data/v9.2"

# 1. Create option set
New-GlobalOptionSet -Token $token -BaseUrl $baseUrl -Name "sprk_mystatus" -DisplayName "My Status" -Description "Status options" -Options @(
    @{ Value = 0; Label = "Draft" },
    @{ Value = 1; Label = "Active" },
    @{ Value = 2; Label = "Archived" }
)

# 2. Create entity
New-Entity -Token $token -BaseUrl $baseUrl -SchemaName "sprk_myentity" -DisplayName "My Entity" -PluralName "My Entities" -Description "Sample entity"

# 3. Add attributes
Add-EntityAttribute -Token $token -BaseUrl $baseUrl -EntityLogicalName "sprk_myentity" -AttributeDef `
    (New-StringAttribute -SchemaName "sprk_code" -DisplayName "Code" -Description "Unique code" -MaxLength 50 -Required $true)

Add-EntityAttribute -Token $token -BaseUrl $baseUrl -EntityLogicalName "sprk_myentity" -AttributeDef `
    (New-MemoAttribute -SchemaName "sprk_notes" -DisplayName "Notes" -Description "Additional notes" -MaxLength 100000)

Add-EntityAttribute -Token $token -BaseUrl $baseUrl -EntityLogicalName "sprk_myentity" -AttributeDef `
    (New-PicklistAttribute -SchemaName "sprk_status" -DisplayName "Status" -Description "Record status" -GlobalOptionSetName "sprk_mystatus")

# 4. Create lookup (if parent entity exists)
New-OneToManyRelationship -Token $token -BaseUrl $baseUrl -ReferencedEntity "sprk_parententity" -ReferencingEntity "sprk_myentity" `
    -LookupSchemaName "sprk_parentid" -LookupDisplayName "Parent" -LookupDescription "Parent record" -Required $true -DeleteBehavior "Cascade"

# 5. Publish
Publish-Customizations -Token $token -BaseUrl $baseUrl -EntityLogicalNames @("sprk_myentity")

Failure Modes & Recovery

Failure Cause Prevention / Recovery
Reference Scripts section points at non-existent paths Earlier version cited scripts under projects/ai-node-playbook-builder/scripts/ — directory doesn't exist Fixed 2026-05-17: Reference Scripts section now points at REAL existing scripts (Deploy-ChartDefinitionEntity.ps1, Deploy-ReportingSchema.ps1, Create-PlaybookTriggerFields.ps1, Create-RegistrationRequestSchema.ps1).
Web API call fails with "LookupAttributeMetadata cannot be created" Tried to create a lookup via Attributes endpoint Use RelationshipDefinitions endpoint with OneToManyRelationshipMetadata. Pattern is in Core Patterns section.
Script runs but no entity appears in Dataverse Forgot Phase 6 (Publish Customizations) ALWAYS run Publish-Customizations after schema changes. The 5-phase model includes Publish as the final step — skipping it leaves changes unpublished.
pac modelbuilder or Web API returns 401 Azure CLI token expired OR token doesn't have Dataverse audience Refresh with az account get-access-token --resource <DataverseUrl>. If still 401, run /dev-cleanup and re-login.
Schema deployed to dev but not prod Each environment requires its own deployment run Schema scripts ARE the deployment artifacts — run them against each environment's $baseUrl. Don't assume dev→prod sync.
Should I use MCP tools instead of this skill? Confusion between metadata creation (this skill) vs data CRUD (MCP) This skill = schema/metadata creation patterns (entities, attributes, relationships). MCP dataverse__* tools = data record CRUD (create_record, update_record, etc.). For schema work, USE THIS SKILL; for data work, USE MCP TOOLS.

Skill created from Task 009 implementation patterns. Reference Scripts section updated 2026-05-17 to point at real existing scripts (per ai-procedure-quality-r1 Phase 2b Wave 2d, decision row 12).