agentsclimarketplace

Dataverse plugin deployment cycle

Skill Genb2nu/Relay/skills/dataverse-plugin-deployment-cycle

Complete Dataverse plugin deployment cycle: compile, upload DLL, register types, register steps, register pre-images, cache flush, verify via trace logs. Covers exact Web API calls, error codes, and the mandatory ordering that prevents runtime failures.From its SKILL.md

Install
npx -y skills add Genb2nu/Relay --skill dataverse-plugin-deployment-cycle

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

9.9 KB, as published. Nobody here has run it

Dataverse Plugin Deployment Cycle

Complete deployment pipeline for C# plugins in Dataverse. Follow this exact order — skipping or reordering steps causes runtime KeyNotFoundException, stale cache, or missing pre-images.


Deployment Order (MANDATORY)

1. Compile DLL (dotnet build --configuration Release)
2. Upload assembly (POST pluginassemblies or PATCH content)
3. Register plugin types (POST plugintypes)
4. Register steps (POST sdkmessageprocessingsteps)
5. Register pre-images (POST sdkmessageprocessingstepimages)
6. Cache flush (deactivate → 2s → reactivate each step)
7. Verify via plugintracelogs

Step 1: Compile

dotnet build --configuration Release
$dllPath = "bin/Release/net462/<AssemblyName>.dll"  # or net472 for newer SDK

Strong-name signing (required for sandbox isolation):

# Generate key if not present
$rsa = [System.Security.Cryptography.RSACryptoServiceProvider]::new(1024)
[System.IO.File]::WriteAllBytes("key.snk", $rsa.ExportCspBlob($true))

Add to .csproj:

<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile>

Step 2: Upload Assembly

$dllBytes = [System.IO.File]::ReadAllBytes($dllPath)
$base64 = [Convert]::ToBase64String($dllBytes)

# Check if exists
$uri = "$orgUrl/api/data/v9.2/pluginassemblies?`$filter=name eq '$asmName'&`$select=pluginassemblyid"
$existing = (Invoke-RestMethod -Uri $uri -Headers $headers).value

if ($existing.Count -gt 0) {
    # UPDATE — ONLY send content field
    # CRITICAL: Do NOT include ishidden, version, or other metadata in PATCH body
    # Sending ishidden causes: "PrimitiveValue node found where StartArray expected"
    $body = @{ content = $base64 } | ConvertTo-Json
    Invoke-RestMethod -Method PATCH `
        -Uri "$orgUrl/api/data/v9.2/pluginassemblies($($existing[0].pluginassemblyid))" `
        -Headers $headers -Body $body -ContentType "application/json"
} else {
    # CREATE
    $body = @{
        name = $asmName
        content = $base64
        isolationmode = 2   # 2 = Sandbox (required for online)
        sourcetype = 0      # 0 = Database
    } | ConvertTo-Json
    $resp = Invoke-RestMethod -Method POST `
        -Uri "$orgUrl/api/data/v9.2/pluginassemblies" `
        -Headers (@{ Authorization = "Bearer $token"; "Content-Type" = "application/json"; "Prefer" = "return=representation" }) `
        -Body $body -ContentType "application/json"
    $asmId = $resp.pluginassemblyid
}

Error codes:

  • 0x8004416c = unsigned assembly (missing strong-name key)
  • 0x80048537 = assembly too large (>16MB limit)

Step 3: Register Plugin Types

One POST per class that implements IPlugin:

$body = @{
    typename = "MyNamespace.MyPlugin"
    friendlyname = "My Plugin"
    name = "MyNamespace.MyPlugin"
    "[email protected]" = "/pluginassemblies($asmId)"
} | ConvertTo-Json

$typeResp = Invoke-RestMethod -Method POST `
    -Uri "$orgUrl/api/data/v9.2/plugintypes" `
    -Headers (@{ Authorization = "Bearer $token"; "Content-Type" = "application/json"; "Prefer" = "return=representation" }) `
    -Body $body -ContentType "application/json"
$typeId = $typeResp.plugintypeid

Step 4: Register Steps

MUST bind BOTH sdkmessageid AND sdkmessagefilterid. Omitting sdkmessagefilterid causes error 0x80040200.

# Get message ID
$msgUri = "$orgUrl/api/data/v9.2/sdkmessages?`$filter=name eq 'Update'&`$select=sdkmessageid"
$msgId = (Invoke-RestMethod -Uri $msgUri -Headers $headers).value[0].sdkmessageid

# Get message filter ID (binds message to specific entity)
$filterUri = "$orgUrl/api/data/v9.2/sdkmessagefilters?`$filter=sdkmessageid/sdkmessageid eq '$msgId' and primaryobjecttypecode eq '<prefix>_<table>'&`$select=sdkmessagefilterid"
$filterId = (Invoke-RestMethod -Uri $filterUri -Headers $headers).value[0].sdkmessagefilterid

$stepBody = @{
    name = "MyPlugin: Update of <prefix>_<table>"
    mode = 0                  # 0 = Synchronous, 1 = Asynchronous
    rank = 1
    stage = 20               # 10 = PreValidation, 20 = PreOperation, 40 = PostOperation
    supporteddeployment = 0  # 0 = ServerOnly
    filteringattributes = "<prefix>_status,<prefix>_approverid"  # ALWAYS set for Update
    "[email protected]" = "/sdkmessages($msgId)"
    "[email protected]" = "/sdkmessagefilters($filterId)"
    "[email protected]" = "/plugintypes($typeId)"
} | ConvertTo-Json

$stepResp = Invoke-RestMethod -Method POST `
    -Uri "$orgUrl/api/data/v9.2/sdkmessageprocessingsteps" `
    -Headers (@{ Authorization = "Bearer $token"; "Content-Type" = "application/json"; "Prefer" = "return=representation" }) `
    -Body $stepBody -ContentType "application/json"
$stepId = $stepResp.sdkmessageprocessingstepid

CRITICAL: Always set filteringattributes on Update steps. Omitting it causes the plugin to fire on EVERY field update — massive performance degradation.


Step 5: Register Pre-Images

Always register pre-images, even if the step already existed. Skipping this causes KeyNotFoundException when the plugin reads context.PreEntityImages["PreImage"].

# Delete existing pre-image first (avoids duplicate key error)
$imgUri = "$orgUrl/api/data/v9.2/sdkmessageprocessingstepimages?`$filter=sdkmessageprocessingstepid/sdkmessageprocessingstepid eq '$stepId' and imagetype eq 0"
$existingImgs = (Invoke-RestMethod -Uri $imgUri -Headers $headers).value
foreach ($img in $existingImgs) {
    Invoke-RestMethod -Method DELETE `
        -Uri "$orgUrl/api/data/v9.2/sdkmessageprocessingstepimages($($img.sdkmessageprocessingstepimageid))" `
        -Headers $headers
}

# Register new pre-image
$imgBody = @{
    name = "PreImage"
    entityalias = "PreImage"
    imagetype = 0                    # 0 = PreImage, 1 = PostImage, 2 = Both
    messagepropertyname = "Target"   # MANDATORY — omitting causes 0x80040203
    attributes = "<prefix>_status,<prefix>_requestorid,<prefix>_approverid"
    "[email protected]" = "/sdkmessageprocessingsteps($stepId)"
} | ConvertTo-Json

Invoke-RestMethod -Method POST `
    -Uri "$orgUrl/api/data/v9.2/sdkmessageprocessingstepimages" `
    -Headers $headers -Body $imgBody -ContentType "application/json"

Error 0x80040203 = missing messagepropertyname. Must be "Target" for Create/Update messages, "Id" for Delete.


Step 6: Cache Flush

Dataverse sandbox caches plugin registrations. After adding/modifying pre-images, the cache must be invalidated:

# Deactivate step
$deactivate = @{ statecode = 1; statuscode = 2 } | ConvertTo-Json
Invoke-RestMethod -Method PATCH `
    -Uri "$orgUrl/api/data/v9.2/sdkmessageprocessingsteps($stepId)" `
    -Headers $headers -Body $deactivate -ContentType "application/json"

# Wait for cache to clear
Start-Sleep -Seconds 2

# Reactivate step
$activate = @{ statecode = 0; statuscode = 1 } | ConvertTo-Json
Invoke-RestMethod -Method PATCH `
    -Uri "$orgUrl/api/data/v9.2/sdkmessageprocessingsteps($stepId)" `
    -Headers $headers -Body $activate -ContentType "application/json"

Without this step, the plugin continues running with the OLD pre-image configuration (or no pre-image) until the sandbox worker recycles naturally (~minutes to hours).


Step 7: Verify via Trace Logs

# CRITICAL: endpoint is plugintracelogs (PLURAL)
# Using singular "plugintracelog" returns 0x80060888 "resource not found"
$traceUri = "$orgUrl/api/data/v9.2/plugintracelogs?`$top=5&`$orderby=createdon desc&`$select=typename,messagename,exceptiondetails,createdon"
$traces = (Invoke-RestMethod -Uri $traceUri -Headers $headers).value

foreach ($t in $traces) {
    Write-Host "$($t.createdon) | $($t.typename) | $($t.messagename) | $($t.exceptiondetails)"
}

Diagnostic: If plugin doesn't fire at all:

  1. Check step is active: statecode eq 0
  2. Check filteringattributes includes the column being updated
  3. Check sdkmessagefilterid points to the correct entity
  4. Check assembly content hash matches the latest DLL

Error Code Reference

CodeMeaningFix
0x8004416cUnsigned assemblyAdd strong-name key to project
0x80040200Missing sdkmessageid or sdkmessagefilterid bindingBind both on step creation
0x80040203Missing messagepropertyname on imageSet to "Target" for Create/Update
0x80048537Assembly exceeds size limitReduce dependencies, ILMerge
0x80060888Resource not foundCheck entity set name (plural form)
KeyNotFoundExceptionPre-image not registered or cached staleRe-register pre-image + cache flush

Reset Test Data Pattern

When resetting test records that a plugin guards (e.g., status transitions):

# 1. Deactivate the plugin step
Invoke-RestMethod -Method PATCH -Uri "$orgUrl/api/data/v9.2/sdkmessageprocessingsteps($stepId)" `
    -Headers $headers -Body '{"statecode":1,"statuscode":2}' -ContentType "application/json"

# 2. Reset the records
Invoke-RestMethod -Method PATCH -Uri "$orgUrl/api/data/v9.2/<prefix>_<table>($recordId)" `
    -Headers $headers -Body '{"<prefix>_status": 0}' -ContentType "application/json"

# 3. Reactivate the plugin step
Invoke-RestMethod -Method PATCH -Uri "$orgUrl/api/data/v9.2/sdkmessageprocessingsteps($stepId)" `
    -Headers $headers -Body '{"statecode":0,"statuscode":1}' -ContentType "application/json"

This avoids the plugin blocking the reset (e.g., "cannot change status from Rejected to Pending").

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.