Automating Intune Primary User UpdatesAutomating Intune Primary User Updates

Laptops get reissued. Someone leaves, their laptop goes back to IT, and a week later it’s on a new starter’s desk. The new user signs in and everything works, except Intune still thinks the laptop belongs to the person who left. Unless somebody remembers to fix it, that primary user field can stay wrong for months.

I ran into this when I went to correct a device by hand and found Change primary user greyed out. That sent me down two paths: working out why Intune wouldn’t let me change it, and then automating the fix so we didn’t have to keep doing it manually.

The result is an Azure Automation runbook that uses a system-assigned managed identity and Microsoft Graph to read recent Windows logon data and find devices where the primary user is probably stale. That word matters. I don’t want automation changing a primary user because an administrator logged on once, so the runbook uses exclusions, account filtering, a logon age limit and a stability window before it changes anything.

This article covers the problem, the Graph data behind it, the setup and permissions, the decision logic, and the safeguards that make it safe to run unattended.

What is the Intune primary user?

The primary user is the person Intune associates with a managed device. Microsoft calls this association device affinity, and for a typical Windows device it’s set during or soon after enrolment. A device with no primary user is treated as a shared device (Microsoft Learn: Find the primary user of an Intune device).

The primary user maps a device to a user in the Company Portal, in the admin centre and in troubleshooting views. When it’s stale, you get wrong ownership information, confusing helpdesk records, inaccurate reporting and odd Company Portal behaviour.

It’s also more than a display field. According to Microsoft’s documentation:

  • It updates the Entra ID device owner. For Entra joined and hybrid joined Windows devices, changing the primary user in Intune also updates it in the Entra ID device blade. The change can take up to 10 minutes to show in both places.
  • It isn’t the enrolled-by user. Changing the primary user doesn’t change the “Enrolled by” user in Intune.
  • It doesn’t change local group membership. A user added to the local Administrators group at join time stays there after you reassign the device. If the previous owner had local admin rights, reassigning the primary user won’t remove them. Handle that with a separate local group membership policy.

The Entra owner link matters more than it first appears. The device owner is who can self-serve BitLocker recovery keys for that device in Entra ID, so an accurate primary user also keeps that self-service pointing at the right person.

Why is “Change primary user” greyed out?

The portal doesn’t offer Change primary user on every device. Microsoft documents several requirements (Microsoft Learn: Find the primary user of an Intune device):

  • Windows only, and joined. It’s supported on Entra joined and hybrid joined Windows devices. Devices that are only Entra registered aren’t supported.
  • The right permission. Your Intune role needs Managed devices > Set primary user. A custom helpdesk role can have almost everything else and still be missing it.
  • A licensed user. The new primary user must hold an Intune licence.

Some enrolment methods deliberately leave the device without a primary user. Windows devices enrolled through Entra bulk enrolment or Autopilot self-deploying mode are userless by design, and automation shouldn’t “fix” them by assigning whoever logged on last.

If the button is greyed out, work through these in order:

  1. Is the device Windows, and Entra joined or hybrid joined?
  2. Is it actually enrolled in Intune?
  3. Does your role include Managed devices > Set primary user?
  4. Is the device meant to be userless or shared?
  5. Does the intended new user have an Intune licence?

If the device isn’t a supported type, automation won’t fix it.

Why automate it, and why “last logon wins” doesn’t work

You could tell the service desk to update the primary user whenever a laptop is reissued. That works until it depends on somebody remembering, and in an environment where devices are regularly reissued, swapped, rebuilt and returned, the field quickly becomes history rather than current information.

The tempting automation is “find the most recent user and make them the primary user.” I wouldn’t do that. Picture an employee’s laptop on their desk while an IT administrator logs on to fix an application. The administrator is now the most recent user. If the job runs overnight and blindly updates the field, Intune now says the laptop belongs to IT.

The same problem happens with support staff, contractors, shared laptops, and service or test accounts. The automation has to tell the difference between recent activity and evidence of a genuine change of ownership. Calling Graph is the easy part. Deciding when the evidence is strong enough to act is the hard part.

The data Intune already has

The useful signal is the usersLoggedOn collection on the Intune managedDevice object. Each entry holds a userId and a lastLogOnDateTime, and it’s documented in the Microsoft Graph beta endpoint:

GET https://graph.microsoft.com/beta/deviceManagement/managedDevices/{id}?$select=id,deviceName,userId,usersLoggedOn

Sort the entries by lastLogOnDateTime and you have a candidate. That’s a far better signal than guessing from the device name or the previous primary user.

There are two catches to design around.

It’s beta. Microsoft warns that beta APIs change more often and recommends v1.0 where possible. That doesn’t make it unusable, but the runbook must fail safely. If the property is missing, skip the device. Never read missing data as “nobody has logged on, so change it.”

It isn’t real time. The device has to check in with Intune for the data to update, so a laptop that’s been off for a week carries week-old logons. That’s why the runbook needs a maximum logon age.

The safeguards that make it usable

Every candidate has to pass all of these checks before the runbook changes anything. If any check fails, the runbook skips the device.

1. Exclude shared and special-purpose devices. I keep an Entra ID device group for devices that should never be reassigned automatically: shared laptops, kiosks, training and meeting-room devices, test machines, and anything with a deliberately hand-set primary user. The runbook skips every member. That’s far safer than trying to make the script detect special devices on its own.

2. Decide what to do with devices that have no primary user. This is easy to miss. A device with no primary user may be stale, or it may be userless by design, such as one enrolled by bulk enrolment or Autopilot self-deploying mode. My default is to skip devices with no primary user unless they’re in an explicit opt-in group.

3. Exclude administrator and service accounts. Filter by naming pattern, such as admin*, svc-* or test-*, matched to your own conventions. The exclusion should be deliberate, not an assumption that every recent logon is the employee.

4. Ignore disabled and unlicensed accounts. A disabled account should never become the new primary user. The same goes for an account without an Intune licence, since the update will fail anyway.

5. Require a recent logon. A MaxLogonAgeDays value, such as 30 days, stops old Intune data being read as evidence of current ownership. If the newest suitable logon is older than that, skip the device.

6. Use a stability window. This is the most important safeguard. Before changing anything, the runbook asks whether the current primary user has stopped using the device for a meaningful period, set by a StabilityDays value such as 14 days. If the current primary user has logged on within that window, the device is left alone.

That’s the anti-flapping rule. A laptop genuinely shared by two people stays as it is, rather than bouncing between them every night. The primary user only changes when the evidence says the previous user has really moved on.

Put together, a change only happens when all of these are true: a valid, licensed, enabled user logged on recently; they aren’t an admin or service account; the device isn’t excluded; and the current primary user is outside the stability window.

The identity: a managed identity with Graph permissions

Azure Automation is a natural home for this. A system-assigned managed identity gives the Automation account its own identity in Entra ID, so there’s no password, client secret or certificate stored anywhere.

To enable it, go to Automation account > Account Settings > Identity, set the system-assigned status to On and save (Microsoft Learn: Enable managed identities for your Automation account). Note the object ID it shows.

Granting Microsoft Graph permissions

This is the less obvious part. A managed identity doesn’t have the API permissions blade you’d use for an app registration. Instead, you assign Graph application permissions to its service principal as app role assignments, using PowerShell:

Connect-MgGraph -Scopes "Application.Read.All","AppRoleAssignment.ReadWrite.All"

$miObjectId = "<managed identity object id>"

# Microsoft Graph's well-known application ID
$graphSp = Get-MgServicePrincipal -Filter "appId eq '00000003-0000-0000-c000-000000000000'"

$permissions = @(
    "DeviceManagementManagedDevices.ReadWrite.All"
    "User.Read.All"
    "GroupMember.Read.All"
)

foreach ($permission in $permissions) {
    $role = $graphSp.AppRoles | Where-Object {
        $_.Value -eq $permission -and
        $_.AllowedMemberTypes -contains "Application"
    }

    if (-not $role) {
        throw "Graph application permission not found: $permission"
    }

    New-MgServicePrincipalAppRoleAssignment `
        -ServicePrincipalId $miObjectId `
        -PrincipalId $miObjectId `
        -ResourceId $graphSp.Id `
        -AppRoleId $role.Id
}

AppRoleAssignment.ReadWrite.All is itself a powerful permission, so run this from a controlled administrative session.

PermissionWhy the runbook needs it
DeviceManagementManagedDevices.ReadWrite.AllRead devices and their logon data, and update the primary user
User.Read.AllResolve candidate accounts and check they’re enabled and licensed
GroupMember.Read.AllRead the exclusion group

DeviceManagementManagedDevices.Read.All would cover the reads, but updating the primary user needs the write permission. Treat that as privileged. Anyone who can edit the runbook can effectively use the identity, so runbook authoring rights matter as much as the Graph permissions themselves.

The key code

These are the building blocks of the runbook.

Getting a Graph token

Disable-AzContextAutosave -Scope Process | Out-Null
Connect-AzAccount -Identity | Out-Null

function Get-GraphHeaders {
    $tokenObj = Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com"
    if ($tokenObj.Token -is [securestring]) {
        $token = ConvertFrom-SecureString $tokenObj.Token -AsPlainText
    } else {
        $token = $tokenObj.Token
    }
    @{ Authorization = "Bearer $token" }
}

$headers = Get-GraphHeaders

The SecureString check is worth keeping. From Az.Accounts 5.0.0, Get-AzAccessToken returns the token as a SecureString by default (Microsoft Learn: Migration guide for Az 14.0.0). Handling both types keeps the runbook working across module updates.

Wrapping it in a function also lets you refresh the token. Access tokens expire after roughly an hour, and a runbook making one call per device across a large fleet can outlast one. Call Get-GraphHeaders again every few hundred devices, or when you get a 401.

Paging through Windows devices

$uri = "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=operatingSystem eq 'Windows'&`$select=id,deviceName,userId"
$devices = @()

do {
    $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get
    $devices += $response.value
    $uri = $response.'@odata.nextLink'
} while ($uri)

Graph returns results in pages. If you don’t follow @odata.nextLink, the runbook will look like it works while quietly ignoring most of a large fleet.

Reading logons, and skipping safely

$detail = Invoke-RestMethod -Headers $headers -Method Get `
    -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$($device.id)?`$select=id,deviceName,userId,usersLoggedOn"

if (-not $detail.usersLoggedOn) {
    Write-Warning "No usersLoggedOn data for $($device.deviceName). Skipping."
    continue
}

$latest = $detail.usersLoggedOn |
    Sort-Object { [datetime]$_.lastLogOnDateTime } -Descending |
    Select-Object -First 1

$latest.userId is now a candidate, not a decision. It still has to pass every safeguard.

Updating the primary user

$body = @{
    "@odata.id" = "https://graph.microsoft.com/beta/users/$candidateId"
} | ConvertTo-Json

Invoke-RestMethod -Method Post -Headers $headers `
    -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$($device.id)/users/`$ref" `
    -Body $body -ContentType "application/json"

I use the beta endpoint for the update, matching the reads. It’s the version most commonly documented and used for this operation. Also be aware that the userId property on a v1.0 device record isn’t always a reliable view of the current primary user after a change, and Microsoft’s own Q&A recommends reading it through the beta users relationship instead (Microsoft Q&A: primary user not updating in Graph).

This call doubles as a diagnostic. If the portal option is greyed out, testing it on one known device tells you whether the operation works for your automation identity. Don’t go straight from one successful test to the whole fleet.

Handling throttling

One request per device across thousands of devices will eventually hit Graph throttling. When Graph returns HTTP 429, it includes a Retry-After header. Wrap your calls so they wait that long and retry, rather than failing the whole job.

Dry run first, then roll out in stages

The first time the runbook runs, it shouldn’t change anything. Start with $DryRun = $true and have it log every decision:

Device: LAPTOP-042
Current primary user: old.user@example.com
Candidate: new.user@example.com (last logon 25/09/2026 08:42)
Current user last logon: 18/07/2026 14:21
Decision: WOULD CHANGE

Device: LAPTOP-117
Candidate: administrator@example.com
Decision: SKIP - excluded account

Device: LAPTOP-203
Candidate: another.user@example.com
Decision: SKIP - current primary user logged on within stability window

That output makes the runbook auditable, and it shows you where your assumptions don’t match your environment. Keep the dry-run switch permanently. It’s just as useful later, when you change the exclusion rules or investigate an odd result.

I’d roll it out in stages:

  1. Build the identity. Create the Automation account and enable the managed identity.
  2. Grant permissions. Only the three the runbook needs.
  3. Test one device. Pick one where you know the right answer, and confirm you can read it, read usersLoggedOn, resolve the candidate and make the update.
  4. Dry run the fleet. Collect proposed decisions for a couple of weeks without changing anything.
  5. Tune the exclusions. This is where you find the real-world exceptions: admin patterns, service accounts, shared devices and kiosks.
  6. Go live on a limited scope. Start with a controlled group of devices, not the whole fleet.
  7. Enable changes fleet-wide. Only once the dry-run output makes sense.

When it’s live, publish the runbook and schedule it once a day, overnight. A runbook that’s saved but not published won’t run on a schedule, even though it works in the Test pane. Running more often than daily adds nothing, because the logon data isn’t real time, and the goal is correcting stale assignments rather than tracking users live. Set DryRun to False in the schedule’s parameters rather than editing the script.

Monitoring and troubleshooting

A scheduled job that fails silently is worse than no automation. If everyone believes primary users are being maintained, but the runbook has been failing for three months, nobody notices the data going stale.

At a minimum, alert on failed Automation jobs, and log authentication failures, Graph permission errors, throttling and unexpected exceptions. Report the number of changes, skips and exclusions on each run, and log every actual change so you can answer “why does Intune say this laptop belongs to that person?”:

27/09/2026 01:04:22
Device: LAPTOP-042
Previous primary user: old.user@example.com
New primary user: new.user@example.com
Reason: current primary user outside stability window

If it works in the Test pane but not on schedule, check these:

  • Nothing runs. The runbook hasn’t been published, or the published version is out of date.
  • Authentication fails. Check the managed identity is enabled, the token request succeeds, and the permissions were assigned to the right service principal.
  • Graph returns 403. The identity authenticates but lacks a permission. Check the app role assignments on the managed identity, not on your own admin account.
  • Only some devices are processed. You aren’t following @odata.nextLink, or the token expired partway through a long run.
  • The wrong user is selected. Tune the safeguards rather than the Graph query. The decision logic is almost always the part that needs adjusting.

What I wouldn’t automate

There’s a temptation to keep making the script smarter: usage scores, every sign-in event, application usage, or even machine learning to decide who owns a laptop. I don’t think any of it is needed. The value of this automation is that it handles the obvious cases. If the evidence is ambiguous, leave the device alone. That’s much easier to trust.

I’d also keep two related changes out of scope:

  • Autopilot assigned user. It’s a separate property from the Intune primary user, and changing one doesn’t change the other. If you assign Autopilot users during provisioning, treat that as its own decision.
  • Local administrator rights. As covered earlier, reassigning the primary user doesn’t touch local group membership. Manage that with its own policy.

Two scenarios side by side

Here’s the kind of device the automation is built for. The current primary user left months ago and their account is disabled. A new employee logs on daily. The old user is well outside the stability window and the device isn’t excluded. That’s a clear, safe correction.

Compare that with a laptop where the current primary user logs on every day, and an administrator logged on yesterday to troubleshoot. The evidence is ambiguous, so the right decision is to skip it.

The Graph call is the easy part

The request that changes the primary user is simple. The hard part is deciding whether to make it at all, and that’s the difference between useful automation and a script that just creates a new set of wrong data.

A laptop that’s been reissued shouldn’t keep pointing at someone who left six months ago. A shared laptop shouldn’t change hands every time an administrator logs on. Use Intune’s own logon data as the starting point, filter out the obvious exceptions, require recent activity, give the current primary user a stability window, and leave anything ambiguous alone.

Checklist: automating the Intune primary user

  • Confirm the device is Windows and Entra joined or hybrid joined.
  • Check your role includes Managed devices > Set primary user.
  • Confirm the new user has an Intune licence.
  • Know the difference between primary user, enrolled-by user, and Autopilot assigned user.
  • Remember that changing the primary user updates the Entra device owner, but not local group membership.
  • Enable a system-assigned managed identity and grant only the Graph permissions you need.
  • Restrict who can edit the runbook.
  • Exclude shared devices, admin and service accounts, and disabled or unlicensed users.
  • Decide how to handle devices with no primary user.
  • Use a maximum logon age and a stability window.
  • Treat missing usersLoggedOn data as a reason to skip.
  • Follow @odata.nextLink, refresh the token on long runs and retry on 429.
  • Start in dry-run mode, then roll out in stages.
  • Publish the runbook, schedule it daily, and alert on failures.
  • Log every change.

Sources

Leave a Reply

Your email address will not be published. Required fields are marked *