All scripts
Automation 555

Automate New Employee Setup With Microsoft Graph

Creates a new Entra ID user for each row in a CSV, assigns a license, sets their manager, adds them to the right groups, and logs a temporary password for each new hire. Built to run once per hiring batch instead of clicking through the admin center one field at a time.

New-M365UserOnboarding.ps1
<#
.SYNOPSIS
Creates and provisions a new Microsoft 365 user from a CSV file using Microsoft Graph.

.DESCRIPTION
Reads a CSV of new hires, creates each user in Entra ID, assigns a license,
sets their manager, adds them to the groups listed in the CSV, and writes a
summary log with each temporary password. Meant to run once per hiring batch,
not per user.

.AUTHOR
Shehryar Hassan

.EXAMPLE
.\New-M365UserOnboarding.ps1 -CsvPath ".\new-hires.csv" -UsageLocation "US"
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$CsvPath,

    [Parameter(Mandatory = $true)]
    [string]$UsageLocation,

    [Parameter(Mandatory = $false)]
    [string]$LogPath = ".\onboarding-log.csv"
)

if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Users)) {
    Write-Error "Microsoft.Graph.Users module is not installed. Run: Install-Module Microsoft.Graph -Scope CurrentUser"
    return
}

Connect-MgGraph -Scopes "User.ReadWrite.All", "Group.ReadWrite.All", "Directory.ReadWrite.All"

if (-not (Test-Path $CsvPath)) {
    Write-Error "CSV file not found at $CsvPath"
    return
}

$newHires = Import-Csv -Path $CsvPath
$results = @()

foreach ($hire in $newHires) {
    Write-Host "Provisioning $($hire.DisplayName) ($($hire.UserPrincipalName))..." -ForegroundColor Cyan

    $tempPassword = -join ((65..90) + (97..122) + (48..57) | Get-Random -Count 14 | ForEach-Object { [char]$_ })

    $passwordProfile = @{
        Password                      = $tempPassword
        ForceChangePasswordNextSignIn = $true
    }

    $userParams = @{
        AccountEnabled    = $true
        DisplayName       = $hire.DisplayName
        UserPrincipalName = $hire.UserPrincipalName
        MailNickname      = $hire.UserPrincipalName.Split("@")[0]
        JobTitle          = $hire.JobTitle
        Department        = $hire.Department
        UsageLocation     = $UsageLocation
        PasswordProfile   = $passwordProfile
    }

    $status = "Success"
    $errorMessage = ""

    try {
        $newUser = New-MgUser @userParams

        if ($hire.LicenseSkuId) {
            $licenseParams = @{
                AddLicenses    = @(@{ SkuId = $hire.LicenseSkuId })
                RemoveLicenses = @()
            }
            Set-MgUserLicense -UserId $newUser.Id -BodyParameter $licenseParams
        }

        if ($hire.ManagerUpn) {
            $manager = Get-MgUser -UserId $hire.ManagerUpn
            $managerRef = @{
                "@odata.id" = "https://graph.microsoft.com/v1.0/users/$($manager.Id)"
            }
            Set-MgUserManagerByRef -UserId $newUser.Id -BodyParameter $managerRef
        }

        if ($hire.GroupIds) {
            $groupIds = $hire.GroupIds -split ";"
            foreach ($groupId in $groupIds) {
                New-MgGroupMember -GroupId $groupId.Trim() -DirectoryObjectId $newUser.Id
            }
        }
    }
    catch {
        $status = "Failed"
        $errorMessage = $_.Exception.Message
        Write-Warning "Failed to provision $($hire.UserPrincipalName): $errorMessage"
    }

    $results += [PSCustomObject]@{
        DisplayName       = $hire.DisplayName
        UserPrincipalName = $hire.UserPrincipalName
        TempPassword      = $tempPassword
        Status            = $status
        Error             = $errorMessage
    }
}

$results | Export-Csv -Path $LogPath -NoTypeInformation
Write-Host "Done. Log written to $LogPath. Temporary passwords are in that file, treat it as sensitive and delete it once passwords are handed off." -ForegroundColor Yellow

Disconnect-MgGraph

Read it before you run it, and test in a safe tenant first.