Thursday, December 18, 2025

Assign Teams with Roles of specific BU using Power shell script

 


<#

.SYNOPSIS

The latest cross BU role mapping feature helps easily mange the cross-bu role mapping, However, there's no tool for bulk cross-bu role assignment, with currently available tools mapping directly to the User's/team's BU. This script associates the users/teams with the roles of the specified BU.

  Assign Security Roles to Teams in Dataverse and relate the Field Security (Column Security) Profile

  "Field Service Resource" to those teams. PowerShell 5.1 compatible; robust paging; safe OData URLs.

  - Uses OAuth2 client credentials (ClientId/ClientSecret/TenantId) if AccessToken not provided.

  - Prefetches ALL Teams, Roles (with BU GUID), Business Units, and Field Security Profiles.

  - Associates roles via teamroles_association/$ref (fallback to teamroles/$ref).

  - Associates Field Security Profile via teamprofiles_association/$ref.

#>


param(

  [Parameter(Mandatory=$true)]

  [string]$url,  # e.g., https://yourorg.crm6.dynamics.com


  # Option A: Pre-supplied token (if omitted, client credentials are used)

  [string]$AccessToken,


  # Option B: Client credentials (used if AccessToken is empty)

  [Parameter(Mandatory=$true)]

  [string]$applicationId,

  [Parameter(Mandatory=$true)]

  [string]$clientSecret,

  [Parameter(Mandatory=$true)]

  [string]$tenantId

)


# -------------------------- CONFIG --------------------------

# Field Security (Column Security) Profile to relate to each team in TeamRoleMap

$FieldSecurityProfileName = "Field Service - Resource"

# ------------------------------------------------------------

# -------- Install --------

try {

  Install-Module Microsoft.Xrm.Data.PowerShell -RequiredVersion "2.8.21" -Force -Scope CurrentUser -AllowClobber

} catch { }

# -------- Install --------

# -------- Token acquisition (client credentials if needed) --------

function Get-AccessTokenWithClientCredentials([string]$tenantId, [string]$applicationId, [string]$clientSecret, [string]$url) {

  if ([string]::IsNullOrWhiteSpace($tenantId) -or [string]::IsNullOrWhiteSpace($applicationId) -or [string]::IsNullOrWhiteSpace($clientSecret)) {

    throw "Missing TenantId/ClientId/ClientSecret for client-credentials flow."

  }

  $tokenEndpoint = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"

  $body = @{

    grant_type    = "client_credentials"

    client_id     = $applicationId

    client_secret = $clientSecret

    scope         = "$url/.default"

  }

  try {

    $resp = Invoke-RestMethod -Method POST -Uri $tokenEndpoint -Body $body -ContentType "application/x-www-form-urlencoded"

    if (-not $resp.access_token) { throw "Token response missing access_token." }

    return $resp.access_token

  } catch {

    throw "Token acquisition failed: $($_.Exception.Message)"

  }

}


if ([string]::IsNullOrWhiteSpace($AccessToken)) {

  $AccessToken = Get-AccessTokenWithClientCredentials -tenantId $tenantId -applicationId $applicationId -clientSecret $clientSecret -url $url

}


# -------- Helpers --------

function Normalize([string]$s) {

  if ([string]::IsNullOrWhiteSpace($s)) { return "" }

  $t = $s.Trim()

  return $t 

}


# Standard headers for Dataverse Web API

$HeadersCommon = @{

  "Authorization"    = "Bearer $AccessToken"

  "Accept"           = "application/json"

  "OData-MaxVersion" = "4.0"

  "OData-Version"    = "4.0"

}

$JsonHeaders = $HeadersCommon.Clone()

$JsonHeaders["Content-Type"] = "application/json; charset=utf-8"


# --------Sample - TeamRoleMap (edit as needed) --------

$TeamRoleMap = @{

  "Operations" = @(

    @{ roleName = "Base Role";    buName = "Operations BU" }

    @{ roleName = "Base Role";    buName = "Finance BU" }

    @{ roleName = "Staff Role";   buName = "Operations BU" }

    @{ roleName = "Staff Role";   buName = "Finance BU" }

    @{ roleName = "Manager Role"; buName = "Finance BU" }

    @{ roleName = "Manager Role"; buName = "Operations BU" }

  )

  "Finance Team" = @(

    @{ roleName = "Base Role";  buName = "Operations BU" }

    @{ roleName = "Base Role";  buName = "Finance BU" }

    @{ roleName = "Staff Role"; buName = "Operations BU" }

    @{ roleName = "Staff Role"; buName = "Finance BU" }

  )

}


# -------- Paging helper (follows @odata.nextLink) --------

function Invoke-DvGetAll([string]$Uri) {

  $results = @()

  $next = $Uri

  while ($next) {

    try {

      $resp = Invoke-RestMethod -Method GET -Uri $next -Headers $HeadersCommon

    } catch {

      throw "GET failed for '$next': $($_.Exception.Message)"

    }

    if ($resp.value) { $results += $resp.value }

    $next = $resp.'@odata.nextLink'

  }

  return $results

}


# -------- Prefetch: Teams, Roles, BUs, Field Security Profiles --------

$teamsBase = "$url/api/data/v9.2/teams"

$rolesBase = "$url/api/data/v9.2/roles"

$busBase   = "$url/api/data/v9.2/businessunits"

$fspBase   = "$url/api/data/v9.2/fieldsecurityprofiles"  # entity set path (Web API ref)


$teams = Invoke-DvGetAll ($teamsBase + '?$select=teamid,name')

$roles = Invoke-DvGetAll ($rolesBase + '?$select=roleid,name,_businessunitid_value')

$businessUnits = Invoke-DvGetAll ($busBase + '?$select=businessunitid,name')

$fieldSecProfiles = Invoke-DvGetAll ($fspBase + '?$select=fieldsecurityprofileid,name')


# BU GUID -> Name map

$buMap = @{}

foreach ($b in $businessUnits) {

  if ($b.businessunitid) {

    $buName = ""

    if ($null -ne $b.name) { $buName = $b.name }

    $buMap[$b.businessunitid] = $buName

  }

}


# Team index: normalized name -> teamid

$teamIndex = @{}

foreach ($t in $teams) {

  $teamIndex[(Normalize $t.name)] = $t.teamid

}


# Role index keyed by "roleName\nbuName"

$roleIndex = @{}

foreach ($r in $roles) {

  $buName = ""

  if ($r._businessunitid_value -and $buMap.ContainsKey($r._businessunitid_value)) {

    $buName = $buMap[$r._businessunitid_value]

  }

  $key = "{0}`n{1}" -f (Normalize $r.name), (Normalize $buName)

  $roleIndex[$key] = $r.roleid

}


# Resolve Field Security Profile ID by name

$fspTarget = $null

$normFspName = Normalize $FieldSecurityProfileName

foreach ($fsp in $fieldSecProfiles) {

  if ((Normalize $fsp.name) -eq $normFspName) { $fspTarget = $fsp; break }

}

$fspId = $null

if ($fspTarget -and $fspTarget.fieldsecurityprofileid) {

  $fspId = [Guid]$fspTarget.fieldsecurityprofileid

  Write-Host ("Field Security Profile found: {0} ({1})" -f $FieldSecurityProfileName, $fspId)

} else {

  Write-Warning ("Field Security Profile not found: {0}. Teams will not be related to a profile." -f $FieldSecurityProfileName)

}


Write-Host ("Prefetched: teams={0}, roles={1}, BUs={2}, FSPs={3}" -f $teamIndex.Count, $roleIndex.Count, $buMap.Count, $fieldSecProfiles.Count)


# -------- Existing role assignments for a Team --------

function Get-TeamExistingRoleIds([Guid]$TeamId) {

  $set = New-Object System.Collections.Generic.HashSet[Guid]

  try {

    $url1 = "$url/api/data/v9.2/teams($TeamId)/teamroles_association" + '?$select=roleid,name'

    $rolesForTeam1 = Invoke-DvGetAll $url1

    if ($rolesForTeam1) { foreach ($rr in $rolesForTeam1) { if ($rr.roleid) { [void]$set.Add([Guid]$rr.roleid) } } }

    return $set

  } catch { Write-Warning ("teamroles_association fetch failed for TeamId {0}: {1}" -f $TeamId, $_.Exception.Message) }

  try {

    $url2 = "$url/api/data/v9.2/teams($TeamId)/teamroles" + '?$select=roleid,name'

    $rolesForTeam2 = Invoke-DvGetAll $url2

    if ($rolesForTeam2) { foreach ($rr in $rolesForTeam2) { if ($rr.roleid) { [void]$set.Add([Guid]$rr.roleid) } } }

  } catch { Write-Warning ("teamroles fetch failed for TeamId {0}: {1}" -f $TeamId, $_.Exception.Message) }

  return $set

}


# -------- Existing Field Security Profiles for a Team --------

function Get-TeamExistingFspIds([Guid]$TeamId) {

  $set = New-Object System.Collections.Generic.HashSet[Guid]

  try {

    # M:N nav on Team: teamprofiles_association  (Web API ref)

    $requestUrl = "$url/api/data/v9.2/teams($TeamId)/teamprofiles_association" + '?$select=fieldsecurityprofileid,name'

    $fsps = Invoke-DvGetAll $requestUrl

    if ($fsps) { foreach ($p in $fsps) { if ($p.fieldsecurityprofileid) { [void]$set.Add([Guid]$p.fieldsecurityprofileid) } } }

  } catch { Write-Warning ("teamprofiles_association fetch failed for TeamId {0}: {1}" -f $TeamId, $_.Exception.Message) }

  return $set

}


# -------- Associate a Role to a Team (POST $ref; fallback nav property) --------

function Associate-RoleTo-Team([Guid]$TeamId, [Guid]$RoleId) {

  $roleRef = "$url/api/data/v9.2/roles($RoleId)"

  try {

    $url1 = "$url/api/data/v9.2/teams($TeamId)/teamroles_association/`$ref"

    $bodyJson = @{ "@odata.id" = $roleRef } | ConvertTo-Json -Depth 1

    Invoke-RestMethod -Method POST -Uri $url1 -Headers $JsonHeaders -Body $bodyJson -ErrorAction Stop | Out-Null

    return

  } catch { Write-Warning ("Associate via teamroles_association failed: {0}" -f $_.Exception.Message) }

  try {

    $url2 = "$url/api/data/v9.2/teams($TeamId)/teamroles/`$ref"

    $bodyJson2 = @{ "@odata.id" = $roleRef } | ConvertTo-Json -Depth 1

    Invoke-RestMethod -Method POST -Uri $url2 -Headers $JsonHeaders -Body $bodyJson2 -ErrorAction Stop | Out-Null

    return

  } catch { throw "Associate via teamroles failed: $($_.Exception.Message)" }

}


# -------- Associate Field Security Profile to a Team (POST $ref) --------

function Associate-Fsp-To-Team([Guid]$TeamId, [Guid]$FspId) {

  if ($null -eq $FspId) { throw "FSP Id is null; cannot relate." }

  $fspRef = "$url/api/data/v9.2/fieldsecurityprofiles($FspId)"

  # Collection-valued nav associate (Web API associate doc)

  $requestUrl = "$url/api/data/v9.2/teams($TeamId)/teamprofiles_association/`$ref"

  $bodyJson = @{ "@odata.id" = $fspRef } | ConvertTo-Json -Depth 1

  Invoke-RestMethod -Method POST -Uri $requestUrl -Headers $JsonHeaders -Body $bodyJson -ErrorAction Stop | Out-Null

}


# -------- Drive assignment --------

$stats = [ordered]@{

  Teams = 0; AssignedRoles = 0; SkippedRoles = 0; RoleNotFound = 0; RoleFailures = 0;

  FspAssigned = 0; FspSkipped = 0; FspNotFound = 0; FspFailures = 0

}

$start = [datetime]::Now


foreach ($teamName in $TeamRoleMap.Keys) {

  $stats.Teams++

  $normTeam = Normalize $teamName

  $teamId = $teamIndex[$normTeam]


  Write-Host ""

  Write-Host ("-- {0} --" -f $teamName)


  if (-not $teamId) {

    $stats.RoleNotFound++

    Write-Warning ("Team not found: {0}" -f $teamName)

    continue

  }


  # Existing associations

  $existingRoles = Get-TeamExistingRoleIds -TeamId $teamId

  # Normalize roles to HashSet[Guid] (belt & suspenders)

  if ($existingRoles -isnot [System.Collections.Generic.HashSet[Guid]]) {

    $tmpR = New-Object System.Collections.Generic.HashSet[Guid]

    if ($null -ne $existingRoles) {

      if ($existingRoles -is [Guid]) { [void]$tmpR.Add([Guid]$existingRoles) }

      else { foreach ($g in $existingRoles) { try { [void]$tmpR.Add([Guid]$g) } catch { } } }

    }

    $existingRoles = $tmpR

  }


  $existingFsps = Get-TeamExistingFspIds -TeamId $teamId

  # Normalize FSPs to HashSet[Guid] (fixes Guid .Contains error)

  if ($existingFsps -isnot [System.Collections.Generic.HashSet[Guid]]) {

    $tmpF = New-Object System.Collections.Generic.HashSet[Guid]

    if ($null -ne $existingFsps) {

      if ($existingFsps -is [Guid]) { [void]$tmpF.Add([Guid]$existingFsps) }

      else { foreach ($g in $existingFsps) { try { [void]$tmpF.Add([Guid]$g) } catch { } } }

    }

    $existingFsps = $tmpF

  }


  # ---- Roles for this team ----

  $seen = New-Object System.Collections.Generic.HashSet[string]

  $desired = @()

  foreach ($entry in $TeamRoleMap[$teamName]) {

    $key = "{0}`n{1}" -f (Normalize $entry.roleName), (Normalize $entry.buName)

    if (-not $seen.Add($key)) { continue }

    $desired += [ordered]@{ roleName = $entry.roleName; buName = $entry.buName; key = $key }

  }


  foreach ($d in $desired) {

    $roleId = $roleIndex[$d.key]

    if (-not $roleId) {

      $stats.RoleNotFound++

      Write-Warning ("Role not found: '{0}' ({1})" -f $d.roleName, $d.buName)

      continue

    }

    $guidOk = $false; try { $null = [Guid]$roleId; $guidOk = $true } catch { $guidOk = $false }

    if (-not $guidOk) {

      $stats.RoleFailures++; Write-Warning ("Invalid roleId for '{0}' ({1}): '{2}'" -f $d.roleName, $d.buName, $roleId); continue

    }

    if ($existingRoles.Contains([Guid]$roleId)) {

      $stats.SkippedRoles++; Write-Host ("Skip (role already on team): '{0}' ({1})" -f $d.roleName, $d.buName); continue

    }

    try {

      Associate-RoleTo-Team -TeamId $teamId -RoleId ([Guid]$roleId)

      [void]$existingRoles.Add([Guid]$roleId)

      $stats.AssignedRoles++; Write-Host ("Role OK: '{0}' ({1})" -f $d.roleName, $d.buName)

    } catch {

      $stats.RoleFailures++; Write-Warning ("Role FAIL: '{0}' ({1}) - {2}" -f $d.roleName, $d.buName, $_.Exception.Message)

    }

  }


  # ---- Field Security Profile for this team ----

  if ($null -eq $fspId) {

    $stats.FspNotFound++

    Write-Warning ("Skip FSP relate: profile not found: {0}" -f $FieldSecurityProfileName)

  } else {

    if ($existingFsps.Contains([Guid]$fspId)) {

      $stats.FspSkipped++; Write-Host ("FSP Skip (already related): '{0}'" -f $FieldSecurityProfileName)

    } else {

      try {

        Associate-Fsp-To-Team -TeamId $teamId -FspId $fspId

        [void]$existingFsps.Add([Guid]$fspId)

        $stats.FspAssigned++; Write-Host ("FSP OK: '{0}'" -f $FieldSecurityProfileName)

      } catch {

        $stats.FspFailures++; Write-Warning ("FSP FAIL: '{0}' - {1}" -f $FieldSecurityProfileName, $_.Exception.Message)

      }

    }

  }

}


# ---- Elapsed time (as requested) ----

$secs = ([datetime]::Now - $start).TotalSeconds

Write-Host ""

Write-Host ("Done. Teams={0} Roles: Assigned={1} Skipped={2} NotFound={3} Failures={4} | FSP: Assigned={5} Skipped={6} NotFound={7} Failures={8} Time={9}s" -f `

  $stats.Teams, $stats.AssignedRoles, $stats.SkippedRoles, $stats.RoleNotFound, $stats.RoleFailures,

  $stats.FspAssigned, $stats.FspSkipped, $stats.FspNotFound, $stats.FspFailures, $secs)


Monday, December 8, 2025

SLA doesn't retrigger when the the criteria changes

Behavior:
Once SLA criteria is met, it reaches terminal status and no change to the status happens even if the criteria changes, resulting in newly applicable success conditions not met.

Steps to retrigger SLA when criteria Change:
From the Copilot Admin center App, Enable SLA Recalculate option.

Enabling SLA Recalculate ensures that:
  • The SLA is recalculated whenever the criteria change.
  • A new SLA instance is created, even if the previous SLA condition was already met and its success action executed.
  • This allows subsequent success actions tied to new criteria to run without manual intervention.

SLA is not triggering after solution import

Issue: SLA is not triggering after solution import 

Resolution: The SLAs remain inactive post import, hence
1.Manually activate and set as Default post every import

2. Exclude the SLAs from the solution after the first import, that will avoid this additional step.


Sunday, December 7, 2025

Field mapping is not working on Field Service mobile Online

Issue: This is a known limitation on Field Service Mobile online that the field mappings do not work.

Resolution:
Custom logic is needed to populate the fields.

-Ensure the tables being mapped are enabled for Mobile offline
-And Use Xrm.Webapi.offline.RetrieveRecord to retrieve the record and populate it

Email template image is blurred on import

Issue: The image pasted within an Email template - when moved to another organization - through solution blurs

Resolution : Add the images into a publicly accessible website and refer using href in the Email template

What will not work:
1. Adding it in a webresource - This wont render in the email, As this will need access to CRM.

2. Adding it on Power Pages - as a webtemplate - AS this will still need to go through security gates

Wednesday, February 26, 2025

Principles for Discovery and Design in Dynamics 365 System Implementation

When undertaking a Dynamics 365 implementation, whether it's for a new system or transitioning from an old system, the discovery and design sessions are pivotal. Here are principles for successful discovery and design sessions in any Dynamics 365 project:

1. Deep Business Process Analysis:

   - Conduct in-depth reviews of the client's business processes, employing advanced process mapping techniques to gain a holistic understanding of the operational landscape.

   - Utilize interactive workshops to extract nuanced insights and pinpoint opportunities for streamlining with Dynamics 365.

2. Maximization of Native Capabilities:

   - Prioritize the adoption of Dynamics 365's inherent features, promoting a configuration-first mindset.

   - Reserve custom development for scenarios where native solutions cannot fulfil critical business requirements.

3. Comprehensive Stakeholder Engagement:

   - Implement a structured engagement framework to ensure comprehensive input from a diverse group of stakeholders, fostering a sense of ownership and collaboration in every key decision-making process.

4. Objective-Driven Implementation Strategy:

   - Establish a set of well-defined, measurable goals for each phase of the project, that align with the strategic vision of the organization and the capabilities of Dynamics 365.

5. Adherence to Success by Design Principles:

   - Integrate Microsoft's Success by Design methodology to guide the implementation, leveraging its structured approach and industry insights. More details here: Success-by-Design

6. User-Centric Story Crafting:

   - Develop intricate user narratives that encapsulate the diverse interactions with Dynamics 365, focusing on intuitive user experiences and functional completeness.

7. Iterative Prototyping and Feedback Loops:

   - Employ a rapid prototyping model to construct and refine system designs, utilizing stakeholder feedback to iterate towards an optimal solution.

8. Rigorous Documentation Practice:

   - Maintain meticulous records of all requirements, decisions, and design iterations to serve as a comprehensive knowledge base for the project lifecycle.

Migration Strategy for Transitioning to Dynamics 365:

9. In-Depth Legacy System Evaluation:

   - Perform a detailed analysis of the legacy system, capturing its structure, data intricacies, and interdependencies to inform the migration strategy.

10. Strategic Migration Roadmap Development:

   - Craft a migration blueprint that delineates the migration's objectives, scope, and methodology, complemented by a phased timeline and critical checkpoints.

11. Data Mapping and Transformation:

   - Execute a thorough data mapping exercise to transition data to the new Dynamics 365 environment, ensuring data integrity through meticulous cleansing and transformation processes.

Wednesday, September 4, 2024

Considerations while moving Customer insight Journey Components

1. The Marketing Email templates, Emails, Content blocks can be added as solution components , however, if there's continuous updates made to the Emails/some Emails need to be deleted in the Target environment,  one might face circular dependency between Emails and content blocks.

Keep the Email templates, Emails, Content block out of solution and move them with Configuration Migration Tool

With this, they can be dealt with as data and can be deleted/Updated easily.

2. Journey import - A journey cannot be imported to an environment, if another version already exists in.

solution:

-The Journeys Should always imported with just the Journey Name and the Json. 

Including any other columns might cause the Publishing to fail.

3. Segment Import - A Segment already referred to in a Journey should not be updated.
The Journey referring to it should be Stopped, deleted, 
Segment should be deleted and re-imported.
Then the dependent Journey shall be re-imported.

Upgrade Considerations (On Prem - On Prem)

While assessing Upgrade of System, Key considerations:

Unsupported Code - Of JS and C# -
For On premise, can use a Code to check for references to the unsupported code.

 (For Online, can use the Power-apps solution checker)

Reference to the OOB internal tables/fields that are meant to change - Example - String Map and Object Type Code should be removed/updated to use the recommended ways of the Table Name and Option set instead refer

Reports - Use Shared Data Source, to minimize repetitive steps during deployment

Number Of fields exceeding the maximum allowed - while importing solution from source
(older version)
to the upgrade instance (Newer version) - happens as newer version might have an additional set of system fields 

Ensure there's no Unsupported Customization:
https://learn.microsoft.com/en-us/previous-versions/dynamicscrm-2016/developers-guide/gg328350(v=crm.8)?redirectedfrom=MSDN#Unsupported




Tuesday, November 29, 2022

Migrate User Query (Personal Views) using Kingswaysoft

 It took us to the UAT stage to be sure the View Migration using XRM tool box - Personal View Migration Tool might now be viable when there are 1000s of saved Views, and the Saved Views referring to deleted fields which needs extreme manual monitoring.

Thanks to friends Harshal and Nilesh, WE figured out a way to do from Kingsway soft

The userquery table doesn't appear in the source Connection's Tables list for Dynamics, we need to use the OLEDB connection, but the Destination can be Dynamics Connection itself.

The Migration will be 3 step

1. Migrate(Insert/Upsert) the Userqueries without the owner mapping.

2. Share the UserQueries with the Users using the Principal Object Access table migration(Insert/Upsert),
with Source Data filter - Entity name = userquery

3. Migrate the Owner of the User query so that they get assigned to the Original Owners (Upsert)

At Step 1 we do not map the Owner because, once the Owner is changed, the Share will not work, even if you have System Administrator role .

Also If we try to Update the changes to the View like the criteria or the columns, will not be able to update unless Package running User is the Owner.

The Object Type Code in the Layout xml will need to be updated to the userquery's entity in the Target.
(Else the View will not render, for the Custom Entities - where the Object Type code at the Source Organization and the Target Organization are different.)

Monday, November 28, 2022

CRM Managed solution import fail - Plugin does not contain required types

This happens as the Plugin Assembly is seen as a different one by CRM.
To resolve this:
1.Increase the version of the Solution and Publish at the source, then import to the destination


Thanks to Friend Seshagiri - For arriving at this Simple yet tricky solution