๐ฅ Recipe ยท User & Group Management
Bulk Onboard Users from an Excel File Using a PowerShell and Microsoft Graph Script
Create Entra ID accounts, set profile properties, and assign group memberships in a single scripted pass driven by a simple Excel roster
Complexity
Intermediate
Impact
user-management + automation + entra-id + bulk-operations + onboarding
Context
Why This Matters
New-hire onboarding is one of the most repetitive โ and error-prone โ tasks in a Microsoft 365 tenant. Creating each user by hand in the admin center means manually entering names, setting temporary passwords, typing department and phone data, and then clicking through each group to add the new hire. When HR hands you a batch of five, ten, or fifty starters for the same day, the manual approach is slow, inconsistent, and easy to mess up.
This recipe provides a reusable PowerShell script that reads a plain Excel file (columns: Name, Phone, Groups, Department) and onboards every row into Entra ID via the Microsoft Graph PowerShell SDK. The spreadsheet becomes your source of truth: HR fills it in, you run the script, and the tenant is updated in minutes.
Run this recipe any time you need to bulk-provision accounts โ new-hire waves, intern cohorts, seasonal staff, M&A onboarding, or migrating users from another identity system where you can export a CSV/XLSX of names, phones, departments, and group memberships.
Expected Outcomes
After completing this recipe you will have:
- A parameterized PowerShell script (
Onboard-Users.ps1) that bulk-creates Entra ID users from an Excel roster. - Each new user provisioned with display name, first/last name, UPN, mail nickname, department, business phone, usage location, and a forced-change temporary password.
- Each new user added to all groups listed in the
Groupscolumn (semicolon-separated), with group IDs cached for efficiency. - Idempotent behavior โ re-running the script skips existing users and existing group memberships rather than failing.
- A
-WhatIfdry-run mode so you can preview every create and every group-add before committing. - A console summary table showing per-user status (
CREATED,EXISTED,FAILED) and per-group outcomes.
Risks & Considerations
Warnings and gotchas
- Temporary password handling. The script takes the temp password as a mandatory parameter โ do not hard-code it into the script or check it into source control. Prefer a one-time password generator or
Read-Host -AsSecureStringin production. - UPN collisions. The script builds UPNs as
firstname.lastname@domain. Two users with the same name will collide โ add a middle initial or employee ID column for large rosters. - No license assignment. This recipe creates accounts and sets
usageLocation, but does not assign licenses. Use group-based licensing on the groups you add users to, or extend the script withSet-MgUserLicense. - Group name ambiguity. Groups are resolved by
displayName. If two groups share the same name, the first match wins โ use object IDs ormailNicknamefor disambiguation in larger tenants. - Always dry-run first. Run with
-WhatIfagainst the full spreadsheet before the real run. Eyeball the UPNs and group names in the output. - Compliance. Entra ID creation events are audited. Make sure your change-management process (ticket ID, HR approval) is attached to the run, and do not commit the Excel roster with PII to a public repo.
- Rate limits. For rosters over ~100 users, expect Graph throttling. Add
Start-Sleepbetween iterations or batch with-BatchIdif needed.
Required Permissions
| Permission | Why It's Needed |
|---|---|
| User.ReadWrite.All | Create new user accounts and set department, phone, and password profile. |
| Group.ReadWrite.All | Resolve groups by display name and add new users as members. |
| Directory.Read.All | Look up existing users to detect duplicates before creating. |
The fastest way to get this done: just ask Dex. Copy the prompt below and paste it into your Dex conversation.
For IT Admins
Paste into Dex Pro
Manual alternative in Microsoft 365 Admin Center
If you only have one or two users to onboard, the admin center is fine. For anything larger, use the script.
Step 1: Open the Microsoft 365 admin center
https://admin.microsoft.com โ Users โ Active users
Step 2: Add a user
- Click Add a user.
- Fill in First name, Last name, Display name, and Username (the part before
@). - Choose the domain from the dropdown.
- Set an initial password (either auto-generated or typed) and check Require this user to change their password when they first sign in.
- Click Next.
Step 3: Assign licenses (optional)
Select the Location and tick the licenses this user needs, or choose Create user without product license. Click Next.
Step 4: Optional settings โ profile info
- Expand Profile info.
- Enter Job title, Department, Office, Office phone, and Mobile phone as needed.
- Click Next, review, and Finish adding.
Step 5: Add the user to groups
Active users โ click the new user โ Account tab โ Manage groups
- Click Manage groups.
- Search for each group by name, click to add it.
- Click Save changes.
Repeat for every new hire. For batches of more than a few users, switch to the PowerShell script below.
Microsoft Graph API โ what the script calls under the hood
Each of the steps below is what the PowerShell script executes per user. You can call them directly with any HTTP client (Postman, curl, a custom app) if you prefer a language other than PowerShell.
1. Check if the user already exists
GET https://graph.microsoft.com/v1.0/users?$filter=userPrincipalName eq 'morgan.bennett@contoso.com'&$select=id,displayName,userPrincipalName2. Create the user
POST https://graph.microsoft.com/v1.0/users
Content-Type: application/json
{
"accountEnabled": true,
"displayName": "Morgan Bennett",
"givenName": "Morgan",
"surname": "Bennett",
"mailNickname": "MorganBennett",
"userPrincipalName": "morgan.bennett@contoso.com",
"usageLocation": "US",
"department": "HR",
"businessPhones": ["(312) 907-7873"],
"passwordProfile": {
"forceChangePasswordNextSignIn": true,
"password": "<temp-password>"
}
}Returns the new user object including id, which you'll need for group assignments.
3. Resolve each group by display name
GET https://graph.microsoft.com/v1.0/groups?$filter=displayName eq 'Marketing Internal'&$select=id,displayName4. Add the user to the group
POST https://graph.microsoft.com/v1.0/groups/{group-id}/members/$ref
Content-Type: application/json
{
"@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/{user-id}"
}Returns 204 No Content on success. Returns 400 with One or more added object references already exist if the user is already a member โ treat this as idempotent success.
Pagination and throttling
For rosters larger than 100 rows, expect 429 Too Many Requests responses. Honor the Retry-After header. For batching, use POST /$batch to send up to 20 requests per call.
PowerShell walkthrough
The downloadable script below automates the entire Graph flow described above. Here's how it fits together.
1. Install prerequisites (one-time)
Install-Module Microsoft.Graph -Scope CurrentUser
Install-Module ImportExcel -Scope CurrentUserThe ImportExcel module reads .xlsx directly โ no Excel install required, works on Windows, macOS, and Linux with PowerShell 7+.
2. Prepare your Excel roster
Create a file with these columns (header row required):
| Name | Phone | Groups | Department |
|---|---|---|---|
| Morgan Bennett | (312) 907-7873 | Marketing Internal; All Company | HR |
| Alex Chen | (415) 555-0102 | Engineering; All Company | Engineering |
Multiple groups go in a single cell, separated by semicolons.
3. Run the script (dry-run first)
.\Onboard-Users.ps1 `
-ExcelPath ".\new-hires.xlsx" `
-Domain "contoso.com" `
-TempPassword "ChangeMe123!Temp" `
-WhatIfThe -WhatIf switch prints every action without executing. Review the output carefully.
4. Run for real
.\Onboard-Users.ps1 `
-ExcelPath ".\new-hires.xlsx" `
-Domain "contoso.com" `
-TempPassword "ChangeMe123!Temp"What the script does per row
- Parses the
Nameinto first/last, builds a UPN offirst.last@domainand a mailNickname. - Calls
Get-MgUser -Filterto check for an existing account at that UPN. - If missing, calls
New-MgUserwithDisplayName,GivenName,Surname,UserPrincipalName,MailNickname,UsageLocation,Department,BusinessPhones, and aPasswordProfilewithForceChangePasswordNextSignIn = $true. - Splits the
Groupscell on;, resolves each name viaGet-MgGroup -Filter "displayName eq '...'", caches the ID, and callsNew-MgGroupMember. - Appends a row to an in-memory results list; prints a
Format-Tablesummary at the end.
Output you'll see
Connecting to Microsoft Graph...
Connected.
Found 2 user(s) in the spreadsheet.
Processing: Morgan Bennett (morgan.bennett@contoso.com)
User created.
Added to 'Marketing Internal'
Added to 'All Company'
Processing: Alex Chen (alex.chen@contoso.com)
User created.
Added to 'Engineering'
Added to 'All Company'
Onboarding Summary
============================================================
Name UPN Department Status Groups
---- --- ---------- ------ ------
Morgan Bennett morgan.bennett@contoso.com HR CREATED Marketing Internal (added), All Company (added)
Alex Chen alex.chen@contoso.com Engineering CREATED Engineering (added), All Company (added)Next steps