Onboarding automation: the role profile, the joiner event, and the five parts that break
An operator runbook for automated joiner provisioning in Microsoft Entra ID: the role-profile file, a normalised joiner event, a poll-for-readiness Graph sequence, the Temporary Access Pass trap that locks out day-one starters, and the formula to compute your own time saving.
Joiner automation fails in five predictable places, and none of them is the account-creation call. This runbook gives the two artefacts that carry the whole design, a provisioning sequence that polls for readiness instead of assuming it, and the arithmetic to compute an elapsed-time saving from a specific estate rather than quoting somebody else's.
Everything below was written against Microsoft's published documentation. There is no client population behind these numbers and none is implied. Where a figure would depend on a company's own hiring rate or ticket data, the article gives the formula and stops.
Where the elapsed days actually go
Count the manual joiner steps sitting in a service desk queue and the total is usually a couple of hours of keystrokes. Elapsed time runs to days because those steps are chained. The mailbox waits on the licence, the licence waits on group membership, the SaaS accounts key off the work address the mailbox produces, and the training invite waits on the calendar. Every handoff parks the joiner behind somebody else's inbox.
Running the same serial steps faster only saves keystrokes. Working out which steps are genuinely dependent, then firing everything else concurrently, is what collapses days into one readiness window. The dependency graph is the deliverable. The scripts are downstream of it.
Prerequisites
- Microsoft Entra ID as the identity source, with the Microsoft Graph PowerShell SDK installed and an app registration whose service principal holds admin-consented application permissions
User.ReadWrite.All,GroupMember.ReadWrite.AllandUserAuthenticationMethod.ReadWrite.All. Application permissions are consented once in the tenant; they cannot be requested at connect time. - A certificate or client secret registered against that application, so the run authenticates app-only. Microsoft's app-only guide is explicit that this is the path for unattended scripts.
- The
powershell-yamlmodule, if role profiles are stored as YAML and read by PowerShell as they are below. - Group-based licensing already in place, with one licence group per plan. Note that group-based licensing does not follow nested groups: if licences are assigned to a group that contains other groups, only first-level members are licensed, per the Microsoft 365 admin documentation.
- A Temporary Access Pass policy enabled in the Authentication methods policy, scoped to the group joiners land in. Microsoft documents that a pass can be created for any user, but only users inside the policy can sign in with it.
- An orchestrator you control. n8n's own documentation is explicit that a self-hosted deployment makes infrastructure and maintenance the operator's responsibility, which is also what decides who processes the joiner's personal data.
- A named owner for each role profile. Without one the file rots, and a rotten role profile grants access nobody reviewed.
Step 1: write the role profile
The pipeline depends on a reviewable answer to what a given role receives. Written down, that answer is a file under version control with a diff history and an approver. Left implicit, it gets reconstructed by whoever picks up the ticket.
# role-profiles/sales-engineer.yaml
role: sales-engineer
inherits: standard-employee
auto_approve: true # false routes the joiner to a human decision first
entra_groups: # display names, resolved to object ids at run time;
- all-employees # direct membership only, nested groups are not walked
- sales-team
- crm-write
license_group: lic-m365-business-premium
license_family_ceiling: 300 # Business family cap per tenant, then E3/E5
saas:
- service: hubspot
protocol: scim # RFC 7644 endpoint, provisioned via Entra
role: sales-rep
- service: linear
protocol: scim
role: contributor
- service: notion
protocol: vendor-api # no SCIM path in this estate, adapter required
role: member
channels:
slack: ["#sales", "#sales-engineering", "#announcements"]
first_signin:
method: temporary-access-pass
usable_once: false # multi-use avoids the 10-minute registration limit
lifetime_minutes: 480 # 8 hours, the documented default maximum lifetime
review:
owner: sales-director
last_reviewed: 2026-07-28
Two lines are load-bearing and both are dated claims. The 300 in license_family_ceiling comes from the Microsoft 365 and Office 365 plan options service description: the Business base per-user plans are designed for organisations with up to 300 users, a tenant provisioned for 250 seats of Business Premium may provision only 50 more across the Business family, and Microsoft reserves the right to enforce that limit. Crossing it forces an enterprise plan migration mid-flight, so the ceiling belongs in the file.
The lifetime_minutes: 480 comes from the Temporary Access Pass policy defaults: minimum lifetime one hour, maximum eight hours, default one hour, allowed range 10 to 43,200 minutes.
Step 2: normalise the joiner event
Every HR system emits a different webhook shape. Write one thin adapter per source system that produces a single internal event, and the rest of the pipeline never learns which system fired.
interface JoinerEvent {
externalId: string; // stable HR record id, the idempotency key
firstName: string;
lastName: string;
workEmail: string; // generated to the company convention
role: string; // must match a role-profile filename
manager: { externalId: string; email: string };
location: { country: string; timezone: string };
usageLocation: string; // ISO 3166-1 alpha-2, required before licensing
startDate: string; // ISO 8601
employmentType: 'employee' | 'contractor';
costCenter: string;
source: string; // which adapter emitted this
customFields: Record<string, unknown>;
}
externalId stops a replayed webhook creating a second account. usageLocation is the field most adapters forget: without it licence assignment fails, and the group-based licensing path silently inherits the tenant location instead of the joiner's.
Step 3: provision, then poll for readiness
The Microsoft Graph reference for creating a user documents a 201 Created response carrying the user object. It documents nothing about when the mailbox exists, when the licence is active, or when group membership becomes visible downstream, because those are separate subsystems on their own clocks. Microsoft never promised immediacy there. Readiness is a signal you have to go and observe.
# Microsoft.Graph PowerShell SDK, app-only. -Scopes is the interactive path and
# would block the orchestrator on a sign-in prompt, so connect with a credential.
Connect-MgGraph -TenantId $env:JOINER_TENANT_ID -ClientId $env:JOINER_CLIENT_ID -CertificateThumbprint $env:JOINER_CERT_THUMBPRINT
$roleProfile = Get-Content ./role-profiles/sales-engineer.yaml -Raw | ConvertFrom-Yaml
$upn = "jane.doe@contoso.com"
# 1. Create the identity. 201 Created says the object exists, nothing more.
$user = New-MgUser -BodyParameter @{
accountEnabled = $true
displayName = "Jane Doe"
mailNickname = "jane.doe"
userPrincipalName = $upn
usageLocation = "MT"
passwordProfile = @{ forceChangePasswordNextSignIn = $true
password = (New-Guid).Guid }
}
# 2. Resolve the profile's display names to object ids, then add direct membership.
# Nested groups are not walked by the provisioning service or by group licensing.
$groupNames = @($roleProfile.entra_groups) + @($roleProfile.license_group)
foreach ($name in $groupNames) {
$match = @(Get-MgGroup -Filter "displayName eq '$name'")
if ($match.Count -ne 1) {
throw "Group '$name' resolved to $($match.Count) objects. Fix the role profile."
}
New-MgGroupMember -GroupId $match[0].Id -DirectoryObjectId $user.Id
}
# 3. Poll to a deadline. Never block the whole chain on a background subsystem.
$deadline = (Get-Date).AddMinutes(60)
do {
Start-Sleep -Seconds 60
$state = Get-MgUser -UserId $user.Id -Property "id,mail,assignedLicenses"
$ready = ($state.AssignedLicenses.Count -gt 0) -and
(-not [string]::IsNullOrEmpty($state.Mail))
Write-Output ("{0:HH:mm:ss} licences={1} mail={2}" -f (Get-Date),
$state.AssignedLicenses.Count, $state.Mail)
} until ($ready -or (Get-Date) -gt $deadline)
if (-not $ready) {
throw "Readiness deadline passed for $upn. Raise a ticket, do not continue."
}
# 4. Only now issue the first sign-in credential, timed to the start date.
New-MgUserAuthenticationTemporaryAccessPassMethod -UserId $user.Id -BodyParameter @{
isUsableOnce = $false
startDateTime = "2026-08-03T07:00:00Z"
lifetimeInMinutes = 480
}
Expected output
A correct run leaves five things behind, each checkable without trusting the workflow's own log.
- A user object in Entra ID whose
usageLocationis set and whoseassignedLicensesarray is non-empty. - Direct membership in every group named by the role profile, verifiable with
Get-MgUserMemberOf. - A populated
mailproperty, the first cheap proof that Exchange provisioning has moved. - Entries in the Microsoft Entra provisioning logs for each SCIM-connected application. Those logs record every read and write the provisioning service performs. An absent entry means either the user is out of scope or the cycle covering them has not run yet, so check the job's current cycle status before concluding a scoping error.
- A Temporary Access Pass starting at the joiner's first working hour, and a run whose polling loop exited on readiness rather than on the deadline.
The five parts that break
1. Readiness is asynchronous and has no committed time
Microsoft publishes no service level for how long licence and mailbox provisioning takes. The group licensing documentation says processing time varies based on tenant size and current load, and warns that a user removed from a licensed group before being added to the new one stays unlicensed until processing finishes. A table printing a hard figure like fifteen to thirty minutes next to rows measured in seconds is asserting a distribution nobody measured. Poll, set a deadline, escalate on the deadline.
2. The first sign-in paradox
A joiner with no registered authentication method cannot satisfy a policy that requires one. This is where otherwise sound designs stall on day one.
The workable shape is a Conditional Access policy scoped to the Register security information user action, granting on a strength a pass can satisfy, with phishing-resistant strength required on the resources themselves once a passkey or Windows Hello credential exists. Conditional Access supports exactly two user actions, Register security information and Register or join devices, so this is a narrow instrument. It also does not enforce registration; it applies controls when a user attempts one of those actions.
If single-use passes are mandated, budget for two. Microsoft documents that passwordless registration after a one-time pass sign-in must complete within 10 minutes, and that a device enrolment running past that window needs a second pass.
3. SCIM is a floor, not a guarantee
RFC 7644 defines the protocol and Microsoft Entra's provisioning service speaks it, but vendor coverage is uneven. Some applications expose a compliant endpoint, some implement part of the schema, and some offer only a proprietary admin API. Plan for one adapter module per vendor exposing the same three operations, provision, deprovision and check, and keep the variance inside the library.
Only part of the timing is documented. For the assigned-users-and-groups scope, Microsoft gives the approximate initial cycle as a minimum of 0.01 and a maximum of 0.08 minutes multiplied by the number of assigned users, groups and group members in scope. That is a whole-job figure, computed once, not a per-user rate. Errors retry on the next sync cycle at a gradually reduced frequency, and a job failing consistently enough to enter quarantine drops to one cycle per day and is disabled after four weeks in that state. A pipeline treating SCIM as instant reports success while the target application has seen nothing.
4. Licence ceilings constrain the role profile
The 300-seat Business family cap is the common case; the general problem is that a role profile encodes a commercial assumption. Any comment claiming a given tool consumes no paid seat needs a review date beside it, because when the vendor restructures its tiers that comment becomes a budgeting error nobody is watching.
5. The processor boundary
Joiner data is personal data: name, work contact details, role, manager, cost centre. Whoever operates the orchestrator processes it.
Side effects
- Adding a user to a licence group consumes a seat immediately and can fail the batch when the subscription is short. Check available seats before the run, not during it.
- Issuing a Temporary Access Pass replaces any existing pass for that user. Microsoft documents one pass per user, with a new one overriding a valid existing one.
- Removing a user from an application's scope is a deprovisioning event. By default the provisioning service soft-deletes or disables users who fall out of scope, and some connectors send a hard delete instead.
- Group membership changes ripple into anything else keyed on those groups: Conditional Access assignments, SharePoint permissions, Teams membership, other licence groups.
- A polling loop running every 60 seconds per joiner adds Graph request volume. Batch where the API allows it and keep the interval honest.
Rollback
Rollback for a joiner run is an ordered unwind, and the order matters as much as it does on the way in.
- Delete the Temporary Access Pass first, so a half-provisioned identity cannot be signed into while the rest is torn down.
- Remove the SaaS assignments, wait one provisioning cycle, and confirm in the provisioning logs that the deprovisioning writes landed. Reversing this order leaves orphaned accounts in the target applications with no source object left to drive their removal.
- Remove the licence group membership, then the remaining group memberships.
- Soft-delete the user object. Entra ID hard-deletes a user 30 days later and the provisioning service then issues the permanent delete downstream, so a mistake stays recoverable inside that window and a real departure completes without further action.
- Record the run identifier and the reason. The unwind is what an auditor asks to see, and it is the same evidence an offboarding risk check looks for when a leaver's access is questioned months later.
Compute the number for your own estate
No credible figure for time saved can be borrowed from another company, because the inputs are local. Take them from your own ticket system.
- Let M be the median engineer minutes per joiner today, read from closed tickets rather than estimated.
- Let H be joiners per period.
- Let R be the residual minutes per joiner that stay human: exception approvals, hardware handover, anything with no API. R is never zero.
- Recovered engineer minutes per period = (M minus R) times H. Subtract the build and the ongoing maintenance before calling any of it a saving.
- For elapsed time, add up only the steps on the true dependency path: identity, licence, mailbox readiness, then anything keyed to the work address. Everything else runs concurrently and contributes its own duration, not the sum.
- For the SCIM leg, Microsoft publishes only an initial-cycle estimate for a job scoped to assigned users and groups: roughly 0.01 to 0.08 minutes multiplied by the total number of assigned users, groups and group members in scope, for the whole job. There is no published per-user figure for incremental cycles, so take the joiner leg from your own provisioning logs: read the interval between recent incremental cycles for that application and use it as the wait.
Run those lines against real ticket data and the result survives a review. A figure copied from an article will not. The same discipline sits behind how workflow engineering work is scoped: the model comes before the promise.
Limitations
This runbook assumes Microsoft Entra ID is the authoritative identity source and that provisioning flows outward from it. It does not apply unchanged where on-premises Active Directory is authoritative and objects synchronise upward, because the ordering constraints and the writeback path change. It does not cover inbound HR-driven connectors such as Workday or SuccessFactors, which run on their own scheduling model.
Hardware is out of scope on purpose. Laptops have no provisioning API; the workflow can open a ticket and track a date, and that is the whole of the automation.
Regulated sectors requiring segregation of duties before an account exists cannot use auto_approve: true for any role. That removes most of the elapsed-time benefit and leaves the role profile working purely as a review artefact, which is still worth having, but the time argument stops applying.
Every licensing and policy default cited here is dated. Seat models, plan ceilings and authentication method defaults change without notice, and a role profile encoding them without a review date drifts into a wrong answer that still runs cleanly.
Last verified 2026-07-28. Checked against Microsoft Learn for Graph user creation, app-only authentication with the Graph PowerShell SDK, Entra provisioning cycles and quarantine behaviour, Temporary Access Pass defaults, Conditional Access user actions and authentication strengths, the 300-seat Business family cap and group-based licensing, plus RFC 7644, Regulation (EU) 2016/679 and the n8n deployment documentation. Every figure here carries a named source or is the reader's own input.
Sources and further reading
- Microsoft Graph v1.0 reference: Create user
- Use app-only authentication with the Microsoft Graph PowerShell SDK
- Understand how Application Provisioning works in Microsoft Entra ID
- Find out when a specific user is able to access an app in Microsoft Entra Application Provisioning
- Configure a Temporary Access Pass in Microsoft Entra ID
- Overview of Conditional Access authentication strengths
- Targeting resources in Conditional Access policies, including user actions
- Microsoft 365 and Office 365 plan options, including the 300-seat Business family limit
- Assign or unassign licenses to a group in the Microsoft 365 admin center
- RFC 7644: System for Cross-domain Identity Management (SCIM) Protocol
- Regulation (EU) 2016/679 (General Data Protection Regulation)
- n8n documentation: Choose how to use n8n (Cloud versus self-hosted)
Turn the procedure into a tenant decision.
The Architecture Workshop maps the checks, side effects, and rollback path to your own Microsoft 365 environment.
Review the workshopMore from Ops Log
The Microsoft 365 July 2026 price change is really about commitment term
Microsoft reset Microsoft 365 list prices on 1 July 2026. The headline seat numbers moved a little; the commitment term moved a lot. Here are the confirmed figures and the tenant check to run before the next renewal.
A 30-second Conditional Access read and the four gaps it usually surfaces
Four Conditional Access controls decide most of a Microsoft 365 tenant identity posture: admin MFA, legacy-auth block, MFA for all, and a device gate. Here is the read-only check that scores them and what each gap means.
The licence you keep paying for after the seat goes dark
License waste in Microsoft 365 has two honest layers: seats you can reclaim from hard data today, and a term premium you can model but not read from Graph. Here is how I separate them so the number I quote is defensible.