Reassign a Copilot Studio Agent Owner Using PowerShell and the Dataverse Web API
Every Copilot Studio agent is a row in the Dataverse bot table, and like most Dataverse rows it is user-owned. That single fact is the key to a problem most Power Platform admins hit sooner or later: the maker who built an agent has moved teams, left the company, or simply shouldn't be the owner of a production agent any more — and the Copilot Studio UI gives you sharing, not ownership transfer.
This post walks through reassigning an agent owner with PowerShell against the Dataverse Web API, plus a verification script to prove the change landed. It also covers when you should not use this method.
Pick the right approach first
There are three ways to move an agent to a new owner, and they are not equivalent.
| Approach | How it works | Supported? | Side effects |
|---|---|---|---|
| Power Platform API — Reassign | POST .../copilotstudio/environments/{envId}/bots/{botId}/api/botAdminOperations/reassign?api-version=1 | Yes — the documented admin route | New owner gets environment maker permission; old owner loses access to the agent |
| Power Automate — Power Platform for Admins V2 | "Reassign the owner of the bot" action, wrapping the same API | Yes | Same as above; easy to schedule and audit |
Dataverse Web API — PATCH the ownerid | The documented Web API implementation of the Assign message on the bot table | Yes — Assign is listed for the bot table, with PATCH /bots(botid) updating ownerid as its Web API operation | Ownership column changes only — no permission grants, no access revocation |
Use the reassign API when: this is a routine ownership handover and you want the platform to do the full job. Its documented behaviour is that the new owner gets environment maker permissions on the agent's environment and the old owner loses access after reassignment; if the new owner holds the Read transcript privilege, they can also reach the agent's existing transcripts.
Its prerequisites are stricter than most people expect:
| Requirement | Detail |
|---|---|
| Admin role (agents built in Copilot Studio) | Global tenant administrator, AI administrator, Power Platform administrator, or Environment administrator |
| Admin role (agents built in Agent Builder) | Global tenant administrator, AI administrator, or Power Platform administrator |
| App registration | Token must be acquired with the client ID of an app registration granted the CopilotStudio.AdminActions.Invoke scope under the Power Platform API |
| Identifier for the new owner | The Entra user ID — not the Dataverse systemuserid |
| Namespace | Use copilotstudio; the powervirtualagents namespace is deprecated |
The call itself:
POST https://api.powerplatform.com/copilotstudio/environments/{EnvironmentId}/bots/{BotId}/api/botAdminOperations/reassign?api-version=1
{ "NewOwnerAadUserId": "<Entra user ID>" }
It returns no content on success. It does not support classic chatbots — those return 405 Method Not Allowed.
Use the Dataverse PATCH when: you can't meet the app-registration or admin-role prerequisites above, the reassign API fails (a common one is an agent in a managed solution with no unmanaged layer), you're doing a bulk cleanup of orphaned rows, or you're reassigning to a team rather than a user — the reassign API only takes an Entra user ID.
The rest of this post covers the Dataverse route, because that's the one with the fiddly bits.
In a hurry? The complete, copy-paste-ready script — reassign plus verification in a single file — is at the end of this post under Full script. The sections below explain what each part does and why.
Prerequisites
| Requirement | Detail |
|---|---|
| PowerShell | 7.x recommended (5.1 works, with one caveat noted below) |
| Module | Az.Accounts (part of the Az module) |
| Dataverse role | System Administrator, or a role with write privilege on the bot table and the Assign privilege |
| New owner | Must already be an enabled Dataverse user in the target environment — a licensed Entra user alone is not enough |
| Environment URL | Format https://<yourorg>.crm.dynamics.com — find it in the Power Platform admin center under the environment's details |
A note on the new owner: if the target user has never been added to the environment, systemusers returns nothing and the whole script fails at step 3. Add them to a security group or assign a security role first, then wait a few minutes for the user record to sync.
Step 1 — Connect and get a token
The token has to be scoped to the environment URL, not to Azure Resource Manager and not to Graph. This is the single most common mistake — an ARM token returns a 401 that looks like a permissions problem but isn't.
Connect-AzAccount
$envUrl = "https://<yourorg>.crm.dynamics.com"
$tokenObj = Get-AzAccessToken -ResourceUrl $envUrl
if ($tokenObj.Token -is [System.Security.SecureString]) {
$token = [System.Net.NetworkCredential]::new("", $tokenObj.Token).Password
} else {
$token = $tokenObj.Token
}
Why the SecureString branch matters
From Az.Accounts 5.0.0 / Az 14.0.0, the default output type of Get-AzAccessToken changed from PSAccessToken to PSSecureAccessToken — .Token is now a SecureString instead of plain text. Scripts written before that change silently build a header reading Bearer System.Security.SecureString and every call fails with 401.
The type check above makes the script version-agnostic: it works on Az 13 and Az 15 without edits. Worth keeping as a snippet, because this breaks a lot of older automation.
If you're on PowerShell 7 and want to avoid materialising the token in memory as plain text at all, skip the conversion and let Invoke-RestMethod handle the SecureString directly:
$secureToken = (Get-AzAccessToken -ResourceUrl $envUrl).Token
Invoke-RestMethod -Uri $uri -Method Get -Authentication Bearer -Token $secureToken
That parameter set doesn't exist in Windows PowerShell 5.1, which is why the plain-text branch is still the portable option.
Step 2 — Build the headers
$headers = @{
Authorization = "Bearer $token"
"OData-Version" = "4.0"
"OData-MaxVersion" = "4.0"
"Content-Type" = "application/json"
"If-Match" = "*"
}
| Header | Why it's there |
|---|---|
OData-Version / OData-MaxVersion | Required by the Dataverse Web API on every request |
Content-Type | JSON payload for the PATCH |
If-Match: * | Update-only guard. Dataverse PATCH performs an upsert — if the record ID doesn't exist, it creates a row with that ID. Microsoft's own guidance is that this header ensures you don't create a new record by accidentally performing an upsert, so a typo in the GUID fails instead of quietly creating a phantom agent row |
Do not drop If-Match because "the ID is obviously right". It costs nothing and it has saved me from a bad copy-paste more than once.
Step 3 — Find the agent and the new owner
$agentName = "Contoso HR Agent"
$newOwnerUpn = "newowner@contoso.com"
# Find the agent
$botUri = "$envUrl/api/data/v9.2/bots?`$select=botid,name,_ownerid_value&`$filter=name eq '$agentName'"
$bot = Invoke-RestMethod -Method Get -Headers $headers -Uri $botUri
if ($bot.value.Count -eq 0) {
throw "No agent found with name '$agentName' in this environment."
} elseif ($bot.value.Count -gt 1) {
throw "Multiple agents named '$agentName' found. Filter on botid instead."
} else {
$botId = $bot.value[0].botid
Write-Host "Agent found: $($bot.value[0].name) ($botId)" -ForegroundColor Cyan
}
# Find the new owner
$userUri = "$envUrl/api/data/v9.2/systemusers?`$select=systemuserid,fullname,internalemailaddress,isdisabled&`$filter=internalemailaddress eq '$newOwnerUpn'"
$user = Invoke-RestMethod -Method Get -Headers $headers -Uri $userUri
if ($user.value.Count -eq 0) {
throw "User '$newOwnerUpn' is not a Dataverse user in this environment."
} elseif ($user.value[0].isdisabled) {
throw "User '$newOwnerUpn' exists but is disabled — assign a role or re-enable first."
} else {
$userId = $user.value[0].systemuserid
Write-Host "New owner: $($user.value[0].fullname) ($userId)" -ForegroundColor Cyan
}
Two escaping details that trip people up in PowerShell:
| Detail | Rule |
|---|---|
$select, $filter, $expand | Escape the $ with a backtick (`$select) inside double-quoted strings, or PowerShell treats it as a variable and sends a malformed URL |
String values in $filter | Wrap in single quotes; escape an embedded apostrophe by doubling it (O''Brien) |
Also note the mismatch worth knowing: internalemailaddress is usually the UPN, but it isn't guaranteed to be. If a lookup fails for a user you're certain exists, filter on domainname instead, or on azureactivedirectoryobjectid if you have the Entra object ID.
Step 4 — Reassign
$body = @{ "ownerid@odata.bind" = "/systemusers($userId)" } | ConvertTo-Json
Invoke-RestMethod -Method Patch -Headers $headers `
-Uri "$envUrl/api/data/v9.2/bots($botId)" -Body $body
A successful call returns HTTP 204 No Content and no response body — so Invoke-RestMethod prints nothing. Silence here is success, which is precisely why you need the verification step below rather than assuming.
The ownerid column is an Owner-type lookup targeting both systemuser and team, so the same pattern reassigns an agent to a team:
$body = @{ "ownerid@odata.bind" = "/teams($teamId)" } | ConvertTo-Json
Team ownership is often the better answer for production agents — it survives people leaving.
Verification script: confirm the owner after reassignment
This is standalone. Run it in a fresh session if you want a genuinely independent check.
<#
Verify-CopilotAgentOwner.ps1
Confirms the current owner of a Copilot Studio agent (Dataverse bot row).
#>
param(
[Parameter(Mandatory)][string]$EnvironmentUrl, # https://<yourorg>.crm.dynamics.com
[Parameter(Mandatory)][string]$AgentName, # or pass -BotId
[string]$BotId,
[string]$ExpectedOwnerUpn # optional assertion
)
# --- Token (version-agnostic) ---
if (-not (Get-AzContext)) { Connect-AzAccount | Out-Null }
$tokenObj = Get-AzAccessToken -ResourceUrl $EnvironmentUrl
if ($tokenObj.Token -is [System.Security.SecureString]) {
$token = [System.Net.NetworkCredential]::new("", $tokenObj.Token).Password
} else {
$token = $tokenObj.Token
}
# Prefer header asks Dataverse to return friendly names alongside the raw GUIDs
$headers = @{
Authorization = "Bearer $token"
"OData-Version" = "4.0"
"OData-MaxVersion" = "4.0"
Accept = "application/json"
Prefer = 'odata.include-annotations="*"'
}
# --- Resolve the agent ---
if (-not $BotId) {
$uri = "$EnvironmentUrl/api/data/v9.2/bots?`$select=botid&`$filter=name eq '$AgentName'"
$hit = Invoke-RestMethod -Method Get -Headers $headers -Uri $uri
if ($hit.value.Count -ne 1) {
throw "Expected exactly one agent named '$AgentName', found $($hit.value.Count)."
} else {
$BotId = $hit.value[0].botid
}
}
# --- Read the current owner ---
$select = 'botid,name,_ownerid_value,modifiedon,statecode'
$botUri = "$EnvironmentUrl/api/data/v9.2/bots($BotId)?`$select=$select"
$agent = Invoke-RestMethod -Method Get -Headers $headers -Uri $botUri
$ownerId = $agent.'_ownerid_value'
$ownerName = $agent.'_ownerid_value@OData.Community.Display.V1.FormattedValue'
$ownerType = $agent.'_ownerid_value@Microsoft.Dynamics.CRM.lookuplogicalname'
# --- Resolve owner details (user or team) ---
if ($ownerType -eq 'systemuser') {
$ownerUri = "$EnvironmentUrl/api/data/v9.2/systemusers($ownerId)?`$select=fullname,internalemailaddress,isdisabled"
$owner = Invoke-RestMethod -Method Get -Headers $headers -Uri $ownerUri
$ownerUpn = $owner.internalemailaddress
} else {
$ownerUri = "$EnvironmentUrl/api/data/v9.2/teams($ownerId)?`$select=name,teamtype"
$owner = Invoke-RestMethod -Method Get -Headers $headers -Uri $ownerUri
$ownerUpn = "(team)"
}
# --- Report ---
[PSCustomObject]@{
Agent = $agent.name
BotId = $agent.botid
OwnerType = $ownerType
OwnerName = $ownerName
OwnerUpn = $ownerUpn
OwnerId = $ownerId
OwnerEnabled = if ($ownerType -eq 'systemuser') { -not $owner.isdisabled } else { 'n/a' }
ModifiedOn = $agent.modifiedon
} | Format-List
# --- Optional assertion ---
if ($ExpectedOwnerUpn) {
if ($ownerUpn -eq $ExpectedOwnerUpn) {
Write-Host "PASS - owner matches '$ExpectedOwnerUpn'." -ForegroundColor Green
} else {
Write-Warning "FAIL - expected '$ExpectedOwnerUpn' but found '$ownerUpn'."
}
}
Run it:
.\Verify-CopilotAgentOwner.ps1 `
-EnvironmentUrl "https://<yourorg>.crm.dynamics.com" `
-AgentName "Contoso HR Agent" `
-ExpectedOwnerUpn "newowner@contoso.com"
Bonus: audit every agent in the environment
Useful before a reassignment campaign — it surfaces orphaned agents in one shot.
$uri = "$EnvironmentUrl/api/data/v9.2/bots?`$select=name,_ownerid_value,createdon,modifiedon"
$all = Invoke-RestMethod -Method Get -Headers $headers -Uri $uri
$all.value | Select-Object `
@{n='Agent'; e={$_.name}},
@{n='Owner'; e={$_.'_ownerid_value@OData.Community.Display.V1.FormattedValue'}},
@{n='Type'; e={$_.'_ownerid_value@Microsoft.Dynamics.CRM.lookuplogicalname'}},
@{n='Modified'; e={$_.modifiedon}} |
Sort-Object Owner | Format-Table -AutoSize
The Prefer: odata.include-annotations="*" header is what makes this readable, and for ownerid it's not just cosmetic. ownerid is a multi-table (polymorphic) lookup, so _ownerid_value on its own returns a GUID that doesn't tell you whether the owner is a user or a team. The @Microsoft.Dynamics.CRM.lookuplogicalname annotation supplies the related table name, and @OData.Community.Display.V1.FormattedValue supplies the primary name. Note also that you can $expand the ownerid navigation property or $select the _ownerid_value lookup property, but not $select the navigation property itself.
One limit to be aware of on the audit query: without $top or paging, Dataverse returns up to 5,000 rows for a standard table. Fine for most environments, but add paging if you're auditing a large tenant.
Gotchas worth knowing
| Symptom | Cause | Fix |
|---|---|---|
401 Unauthorized on every call | Token scoped to ARM or Graph, or SecureString sent as-is | Scope to the environment URL; use the type check from Step 1 |
Header reads Bearer System.Security.SecureString | Az 14 / Az.Accounts 5 breaking change | Convert the SecureString, or use -Authentication Bearer -Token |
PATCH returns 404 | Wrong botid, or the agent is in a different environment | Confirm with the audit query above |
A new bot row appears out of nowhere | PATCH upsert behaviour with no If-Match | Always send If-Match: * |
Reassign API returns 405 Method Not Allowed | Target is a classic chatbot, which the Reassign API doesn't support | Use the Dataverse PATCH instead |
| Reassign API fails with a generic error | Missing admin role or missing CopilotStudio.AdminActions.Invoke scope, or agent is in a managed solution with no unmanaged layer | Check the role/scope table above; open and save the agent to create an active layer, then retry |
| Owner looks unchanged in the admin center | Admin center caching | Give it 5–15 minutes; the environment itself reflects the change almost immediately |
| New owner still can't open the agent | Dataverse PATCH changes the column, not the permissions | Add the user to the environment maker role, or use the reassign API instead |
My take
A few opinions after doing this more than once.
Ownership and sharing are different things, and the UI blurs it. Sharing an agent with a colleague does not make them the owner; reassigning ownership does not automatically preserve everyone the agent was shared with. Check the share list after any handover.
The PATCH is a scalpel, not a hammer. It changes one column. Everything else the platform normally does during a handover — permission grants, revoking the old owner — does not happen. That's a feature when you're fixing orphaned rows in bulk, and a bug when you thought you'd completed a handover. Know which one you're doing.
Reassign the dependencies too. An agent that calls flows, uses connection references, or writes to custom tables has a whole dependency graph still owned by the departing user. Moving the bot row alone leaves the agent working today and broken the day that account is deleted. Query workflows and connectionreferences for the same _ownerid_value and move them in the same maintenance window.
Own agents with teams, not people. Where the environment's security model allows it, an owning team removes this entire category of work. People change roles; teams don't leave.
Snapshot before you change. Run the audit query and export to CSV before any bulk reassignment. It takes ten seconds and it's the only thing standing between you and "who owned this last Tuesday?"
Full script: Reassign-CopilotAgentOwner.ps1
Everything above in one file — connect, resolve, snapshot the current owner, reassign, verify. Supports -WhatIf so you can dry-run it, and reassignment to either a user or a team.
<#
.SYNOPSIS
Reassigns the owner of a Copilot Studio agent (Dataverse bot row) and verifies the result.
.DESCRIPTION
Updates the ownerid column on the Dataverse bot table via the Web API.
This is a data operation: it changes ownership only. It does NOT grant the new
owner environment maker permissions or revoke the old owner's access — for that,
use the supported Power Platform API reassign endpoint.
.EXAMPLE
.\Reassign-CopilotAgentOwner.ps1 -EnvironmentUrl "https://<yourorg>.crm.dynamics.com" `
-AgentName "Contoso HR Agent" -NewOwnerUpn "newowner@contoso.com" -WhatIf
.EXAMPLE
.\Reassign-CopilotAgentOwner.ps1 -EnvironmentUrl "https://<yourorg>.crm.dynamics.com" `
-BotId "00000000-0000-0000-0000-000000000000" -NewOwnerTeamName "Agent Owners"
.NOTES
Requires: PowerShell 5.1+ and the Az.Accounts module.
Requires: System Administrator (or write + Assign privilege on the bot table).
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory)]
[ValidatePattern('^https://.+\.crm.*\.dynamics\.com/?$')]
[string]$EnvironmentUrl,
[Parameter(ParameterSetName = 'ByName', Mandatory)]
[string]$AgentName,
[Parameter(ParameterSetName = 'ById', Mandatory)]
[string]$BotId,
[string]$NewOwnerUpn,
[string]$NewOwnerTeamName,
[string]$SnapshotPath
)
$ErrorActionPreference = 'Stop'
$EnvironmentUrl = $EnvironmentUrl.TrimEnd('/')
$apiRoot = "$EnvironmentUrl/api/data/v9.2"
if (-not $NewOwnerUpn -and -not $NewOwnerTeamName) {
throw "Specify either -NewOwnerUpn or -NewOwnerTeamName."
} elseif ($NewOwnerUpn -and $NewOwnerTeamName) {
throw "Specify only one of -NewOwnerUpn or -NewOwnerTeamName."
}
#region 1. Authenticate ------------------------------------------------------
if (-not (Get-AzContext)) {
Write-Verbose "No Az context found - signing in."
Connect-AzAccount | Out-Null
}
$tokenObj = Get-AzAccessToken -ResourceUrl $EnvironmentUrl
# Az.Accounts 5.0.0 / Az 14.0.0 changed .Token from String to SecureString.
# This check keeps the script working on both old and new module versions.
if ($tokenObj.Token -is [System.Security.SecureString]) {
$token = [System.Net.NetworkCredential]::new("", $tokenObj.Token).Password
} else {
$token = $tokenObj.Token
}
# Read headers: annotations give friendly names alongside raw GUIDs.
$readHeaders = @{
Authorization = "Bearer $token"
"OData-Version" = "4.0"
"OData-MaxVersion" = "4.0"
Accept = "application/json"
Prefer = 'odata.include-annotations="*"'
}
# Write headers: If-Match "*" makes the PATCH update-only (no accidental upsert).
$writeHeaders = @{
Authorization = "Bearer $token"
"OData-Version" = "4.0"
"OData-MaxVersion" = "4.0"
"Content-Type" = "application/json"
"If-Match" = "*"
}
#endregion
#region 2. Helper: read current owner ---------------------------------------
function Get-AgentOwner {
param([string]$Id)
$select = 'botid,name,_ownerid_value,modifiedon,statecode'
$agent = Invoke-RestMethod -Method Get -Headers $readHeaders `
-Uri "$apiRoot/bots($Id)?`$select=$select"
$ownerId = $agent.'_ownerid_value'
$ownerType = $agent.'_ownerid_value@Microsoft.Dynamics.CRM.lookuplogicalname'
$ownerName = $agent.'_ownerid_value@OData.Community.Display.V1.FormattedValue'
if ($ownerType -eq 'systemuser') {
$o = Invoke-RestMethod -Method Get -Headers $readHeaders `
-Uri "$apiRoot/systemusers($ownerId)?`$select=fullname,internalemailaddress,isdisabled"
$ownerUpn = $o.internalemailaddress
$ownerEnabled = -not $o.isdisabled
} else {
$o = Invoke-RestMethod -Method Get -Headers $readHeaders `
-Uri "$apiRoot/teams($ownerId)?`$select=name,teamtype"
$ownerUpn = '(team)'
$ownerEnabled = 'n/a'
}
[PSCustomObject]@{
Agent = $agent.name
BotId = $agent.botid
OwnerType = $ownerType
OwnerName = $ownerName
OwnerUpn = $ownerUpn
OwnerId = $ownerId
OwnerEnabled = $ownerEnabled
ModifiedOn = $agent.modifiedon
}
}
#endregion
#region 3. Resolve the agent -------------------------------------------------
if ($PSCmdlet.ParameterSetName -eq 'ByName') {
# Escape embedded apostrophes for OData (O'Brien -> O''Brien)
$safeName = $AgentName.Replace("'", "''")
$hit = Invoke-RestMethod -Method Get -Headers $readHeaders `
-Uri "$apiRoot/bots?`$select=botid,name&`$filter=name eq '$safeName'"
if ($hit.value.Count -eq 0) {
throw "No agent named '$AgentName' found in this environment."
} elseif ($hit.value.Count -gt 1) {
throw "Found $($hit.value.Count) agents named '$AgentName'. Re-run with -BotId."
} else {
$BotId = $hit.value[0].botid
}
}
Write-Host "`nAgent resolved: $BotId" -ForegroundColor Cyan
#endregion
#region 4. Resolve the new owner --------------------------------------------
if ($NewOwnerUpn) {
$safeUpn = $NewOwnerUpn.Replace("'", "''")
$u = Invoke-RestMethod -Method Get -Headers $readHeaders `
-Uri "$apiRoot/systemusers?`$select=systemuserid,fullname,internalemailaddress,isdisabled&`$filter=internalemailaddress eq '$safeUpn'"
if ($u.value.Count -eq 0) {
throw "'$NewOwnerUpn' is not a Dataverse user in this environment. Add them to the environment first, then retry."
} elseif ($u.value[0].isdisabled) {
throw "'$NewOwnerUpn' exists but is disabled. Enable the user or assign a security role first."
} else {
$newOwnerId = $u.value[0].systemuserid
$newOwnerName = $u.value[0].fullname
$bindTarget = "/systemusers($newOwnerId)"
}
} else {
$safeTeam = $NewOwnerTeamName.Replace("'", "''")
$t = Invoke-RestMethod -Method Get -Headers $readHeaders `
-Uri "$apiRoot/teams?`$select=teamid,name&`$filter=name eq '$safeTeam'"
if ($t.value.Count -ne 1) {
throw "Expected exactly one team named '$NewOwnerTeamName', found $($t.value.Count)."
} else {
$newOwnerId = $t.value[0].teamid
$newOwnerName = $t.value[0].name
$bindTarget = "/teams($newOwnerId)"
}
}
Write-Host "New owner resolved: $newOwnerName ($newOwnerId)" -ForegroundColor Cyan
#endregion
#region 5. Snapshot the current owner ---------------------------------------
$before = Get-AgentOwner -Id $BotId
Write-Host "`n--- BEFORE ---" -ForegroundColor Yellow
$before | Format-List
if ($SnapshotPath) {
$before | Export-Csv -Path $SnapshotPath -NoTypeInformation -Append
Write-Host "Snapshot appended to $SnapshotPath" -ForegroundColor DarkGray
}
if ($before.OwnerId -eq $newOwnerId) {
Write-Host "`nAgent is already owned by $newOwnerName. Nothing to do." -ForegroundColor Green
return
}
#endregion
#region 6. Reassign ----------------------------------------------------------
$body = @{ "ownerid@odata.bind" = $bindTarget } | ConvertTo-Json
if ($PSCmdlet.ShouldProcess("$($before.Agent) ($BotId)", "Reassign owner to $newOwnerName")) {
try {
# A successful PATCH returns HTTP 204 with no body - silence means success.
Invoke-RestMethod -Method Patch -Headers $writeHeaders `
-Uri "$apiRoot/bots($BotId)" -Body $body | Out-Null
Write-Host "`nPATCH sent." -ForegroundColor Cyan
} catch {
$status = $_.Exception.Response.StatusCode.value__
throw "Reassignment failed (HTTP $status): $($_.ErrorDetails.Message)"
}
} else {
Write-Host "`n-WhatIf: no change made." -ForegroundColor DarkGray
return
}
#endregion
#region 7. Verify ------------------------------------------------------------
Start-Sleep -Seconds 2 # small buffer for read consistency
$after = Get-AgentOwner -Id $BotId
Write-Host "`n--- AFTER ---" -ForegroundColor Yellow
$after | Format-List
if ($after.OwnerId -eq $newOwnerId) {
Write-Host "PASS - owner is now $($after.OwnerName) ($($after.OwnerUpn))." -ForegroundColor Green
} else {
Write-Warning "FAIL - expected $newOwnerName but the row still shows $($after.OwnerName)."
}
Write-Host "`nReminder: this changed ownership only. Check environment maker access," -ForegroundColor DarkGray
Write-Host "the agent's share list, and any flows or connection references still owned" -ForegroundColor DarkGray
Write-Host "by the previous owner.`n" -ForegroundColor DarkGray
#endregion
Dry-run first, then commit:
# Preview
.\Reassign-CopilotAgentOwner.ps1 `
-EnvironmentUrl "https://<yourorg>.crm.dynamics.com" `
-AgentName "Contoso HR Agent" `
-NewOwnerUpn "newowner@contoso.com" `
-WhatIf
# Execute, keeping an audit trail of the previous owner
.\Reassign-CopilotAgentOwner.ps1 `
-EnvironmentUrl "https://<yourorg>.crm.dynamics.com" `
-AgentName "Contoso HR Agent" `
-NewOwnerUpn "newowner@contoso.com" `
-SnapshotPath ".\agent-owner-snapshot.csv"
Reference
Every technical claim in this post was checked against these Microsoft Learn pages.
Hashtags: CopilotStudio, PowerPlatform, Dataverse, PowerShell, MicrosoftCopilot, WebAPI, PowerPlatformAdmin, Automation, Microsoft365, AzurePowerShell, AgentGovernance, LowCode
No comments:
Post a Comment