# dataverse-create-schema
---
description: Create or update Dataverse schema components (entities, attributes, relationships, option sets) via Web API
tags: [dataverse, schema, entities, attributes, relationships, powershell]
techStack: [dataverse, power-platform, powershell, web-api]
appliesTo: ["create entity", "add column", "create table", "dataverse schema", "add field", "create relationship"]
alwaysApply: false
---
## 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
---
## Prerequisites
### 1. Azure CLI Authentication
Required for obtaining Dataverse access tokens:
```powershell
# Login to Azure (if not already)
az login
# Verify account
az account show
```
### 2. PAC CLI Authentication (for verification)
```powershell
# 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
```powershell
# 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
```powershell
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
```powershell
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
```powershell
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
```powershell
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
```powershell
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.
```powershell
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
```powershell
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
```powershell
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
```powershell
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
```powershell
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
```powershell
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.
```powershell
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
```powershell
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
```powershell
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)
```powershell
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:
```powershell
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 { "$_" }) -join ""
$publishXml = @{
"ParameterXml" = "$entityXml"
}
} else {
$publishXml = @{
"ParameterXml" = ""
}
}
Invoke-DataverseApi -Token $Token -BaseUrl $BaseUrl -Endpoint "PublishXml" -Method "POST" -Body $publishXml
Write-Host "Customizations published" -ForegroundColor Green
}
```
---
## 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
The following scripts in the repository demonstrate these patterns:
### Main Schema Deployment
**`projects/ai-node-playbook-builder/scripts/Deploy-PlaybookNodeSchema.ps1`**
- Complete 5-phase deployment script
- Creates option sets, entities, attributes, lookups
- Idempotent (checks for existing items before creating)
### Add Missing Attributes
**`projects/ai-node-playbook-builder/scripts/Fix-PlaybookNodeAttributes.ps1`**
- Pattern for adding attributes to existing entities
- Focused on a single entity or set of entities
- Useful for incremental schema updates
### Create N:N Relationships
**`projects/ai-node-playbook-builder/scripts/Create-NNRelationships.ps1`**
- Pattern for many-to-many relationships
- Includes boolean attribute creation with OptionSet
### Existing Script Pattern Reference
**`scripts/Deploy-ChartDefinitionEntity.ps1`**
- Original pattern script
- Shows entity creation with custom fields
---
## 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:
```powershell
# 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
```powershell
# 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")
```
---
*Skill created from Task 009 implementation patterns. For questions, see the reference scripts in `projects/ai-node-playbook-builder/scripts/`.*