PowerShellintermediateRecipeUpdated 15 Jan 2025
Bulk-create AD users from CSV
Onboard a batch of Active Directory users from a CSV file with sensible defaults, safe password generation, and a dry-run mode.
#active-directory#onboarding#csv#windows-server
The problem
You’ve received a spreadsheet with 40 new employees and someone wants them all created in Active Directory by tomorrow. Clicking through Active Directory Users and Computers isn’t going to scale, and you also don’t want to paste plaintext passwords into a chat window.
This recipe reads a CSV, validates it, generates strong passwords, and
creates each user in the correct OU. It has a -DryRun switch so you can
verify everything before writing to AD.
Prerequisites
- Windows Server with the ActiveDirectory PowerShell module
(
Install-WindowsFeature RSAT-AD-PowerShell) - Permissions to create users in the target OU
- A CSV with at least these columns:
GivenName,Surname,Department,JobTitle
The script
<#
.SYNOPSIS
Bulk-create AD users from a CSV with safe defaults.
.PARAMETER CsvPath
Path to the input CSV.
.PARAMETER TargetOU
Distinguished Name of the OU where accounts will be created.
.PARAMETER DryRun
If set, only validates and prints what *would* happen.
#>
param(
[Parameter(Mandatory)] [string] $CsvPath,
[Parameter(Mandatory)] [string] $TargetOU,
[switch] $DryRun
)
Import-Module ActiveDirectory -ErrorAction Stop
function New-StrongPassword {
# 16 chars, mixed case, digits, symbols. Avoid ambiguous chars.
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%^&*'
-join (1..16 | ForEach-Object { $chars[(Get-Random -Max $chars.Length)] })
}
$rows = Import-Csv -Path $CsvPath
Write-Host "Loaded $($rows.Count) rows from $CsvPath"
foreach ($r in $rows) {
$sam = ($r.GivenName.Substring(0,1) + $r.Surname).ToLower() -replace '[^a-z]',''
$upn = "$sam@contoso.local"
$pw = New-StrongPassword
if (Get-ADUser -Filter "SamAccountName -eq '$sam'" -ErrorAction SilentlyContinue) {
Write-Warning " skip — $sam already exists"
continue
}
$params = @{
Name = "$($r.GivenName) $($r.Surname)"
GivenName = $r.GivenName
Surname = $r.Surname
SamAccountName = $sam
UserPrincipalName = $upn
Path = $TargetOU
Department = $r.Department
Title = $r.JobTitle
AccountPassword = (ConvertTo-SecureString $pw -AsPlainText -Force)
ChangePasswordAtLogon = $true
Enabled = $true
}
if ($DryRun) {
Write-Host "DRYRUN would create $sam ($($params.Name))"
} else {
New-ADUser @params
# Export credentials to a protected file — don't print them.
"$sam,$pw" | Out-File -FilePath ".\onboarding-$(Get-Date -f yyyyMMdd).csv" -Append
Write-Host " created $sam"
}
}
Usage
# Preview first
.\bulk-create.ps1 -CsvPath .\new-hires.csv `
-TargetOU 'OU=Employees,DC=contoso,DC=local' `
-DryRun
# Real run
.\bulk-create.ps1 -CsvPath .\new-hires.csv `
-TargetOU 'OU=Employees,DC=contoso,DC=local'
Why it’s safe-ish
- Dry run first. Never run a bulk script against a directory without it.
- Duplicate detection. If an account already exists, we skip instead of error.
- Passwords don’t touch the console. They go to a local file — which you should then move into a password manager or print locker.
ChangePasswordAtLogonmeans the generated password is a single-use token.
Extensions
- Add an
-EmailTemplateparameter to send welcome mails viaSend-MailMessage. - Hook into HaloPSA / Freshservice to auto-create a matching contact.
- Wrap the script in a scheduled task triggered by a Power Automate flow that watches a SharePoint list.