<# .SYNOPSIS Sets or clears one of the 15 extension attributes on Microsoft Entra ID users. .DESCRIPTION Writes extensionAttribute1-15 (onPremisesExtensionAttributes) through Microsoft Graph. Graph only allows this for cloud-only users. Accounts synced from on-premises Active Directory are skipped with a note, because their values have to be changed in AD. Takes pipeline input, so a CSV with UserPrincipalName and Value columns works as-is. Supports -WhatIf. .PARAMETER UserPrincipalName The user to update. Accepts pipeline input by property name. .PARAMETER AttributeNumber Which extension attribute to write, 1 through 15. .PARAMETER Value The value to store. Leave it empty (or use -Clear) to remove the current value. .PARAMETER Clear Clear the attribute instead of setting it. .EXAMPLE .\Set-EntraUserExtensionAttribute.ps1 -UserPrincipalName jane.doe@contoso.com -AttributeNumber 15 -Value 'Cost Center 4410' .EXAMPLE Import-Csv .\attributes.csv | .\Set-EntraUserExtensionAttribute.ps1 -AttributeNumber 10 -WhatIf #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Set')] param( [Parameter(Mandatory, ValueFromPipelineByPropertyName)] [Alias('UPN', 'UserId')] [string]$UserPrincipalName, [Parameter(Mandatory)] [ValidateRange(1, 15)] [int]$AttributeNumber, [Parameter(Mandatory, ValueFromPipelineByPropertyName, ParameterSetName = 'Set')] [AllowEmptyString()] [ValidateLength(0, 1024)] [string]$Value, [Parameter(Mandatory, ParameterSetName = 'Clear')] [switch]$Clear ) begin { if (-not (Get-MgContext)) { Connect-MgGraph -Scopes 'User.ReadWrite.All' -NoWelcome } $attributeName = "extensionAttribute$AttributeNumber" } process { $newValue = if ($Clear -or [string]::IsNullOrWhiteSpace($Value)) { $null } else { $Value.Trim() } $result = [pscustomobject]@{ UserPrincipalName = $UserPrincipalName Attribute = $attributeName OldValue = $null NewValue = $newValue Result = $null } try { $user = Get-MgUser -UserId $UserPrincipalName -Property 'Id,UserPrincipalName,OnPremisesSyncEnabled,OnPremisesExtensionAttributes' -ErrorAction Stop } catch { $result.Result = "Failed: $($_.Exception.Message)" return $result } $oldValue = $user.OnPremisesExtensionAttributes.$attributeName $result.OldValue = $oldValue if ($user.OnPremisesSyncEnabled) { $result.Result = 'Skipped: synced from on-prem AD, change it there' } elseif ($oldValue -ceq $newValue) { $result.Result = 'Unchanged' } elseif ($PSCmdlet.ShouldProcess($user.UserPrincipalName, "Set $attributeName to '$newValue'")) { # Invoke-MgGraphRequest sends a real JSON null, which is how Graph clears the value. $body = @{ onPremisesExtensionAttributes = @{ $attributeName = $newValue } } | ConvertTo-Json -Depth 3 try { Invoke-MgGraphRequest -Method PATCH -Uri "v1.0/users/$($user.Id)" -Body $body -ContentType 'application/json' -ErrorAction Stop | Out-Null $result.Result = 'Updated' } catch { $result.Result = "Failed: $($_.Exception.Message)" } } else { $result.Result = 'WhatIf' } Write-Verbose "$($user.UserPrincipalName): $($result.Result)" $result }