
Batch-Create Active Directory Users from CSV with PowerShell
Before you run this
This guide gives you a PowerShell script that reads a CSV file of new employees and creates one Active Directory user account per row, with a per-row try/catch so that one bad line doesn't halt the whole batch. Its purpose is bulk onboarding — creating dozens or hundreds of accounts without clicking through the console each time.
It needs elevated privileges. Creating AD accounts requires an account with delegated rights to create user objects in the target OU — in most shops that means a Domain Admin or an account explicitly delegated "Create User objects" on that OU. Run it from an elevated PowerShell session (Run as administrator) on a domain controller, or from an admin workstation with RSAT / the ActiveDirectory module installed. The script imports that module; if Import-Module ActiveDirectory fails, you don't have RSAT and the rest won't run.
This changes your directory. New-ADUser writes real objects into AD. It is not a dry run. Deleting a batch of freshly created users afterward is possible (I show how at the end), but AD does not have an "undo" button — if you delete the wrong account you're into AD Recycle Bin recovery or backups. So:
- Read the script before you run it. Understand what each field maps to.
- Test on a throwaway OU first. Create an OU like
OU=Onboarding-Testin a lab domain or a non-production OU, point the script at it with a two-row CSV, and confirm the results before you ever run it against a real staff OU with a real list. - Passwords in the CSV are plaintext on disk. Treat the CSV as a secret. Store it somewhere access-controlled, and delete it (or shred it) once onboarding is done.
Assumptions
- Windows Server 2019/2022 acting as (or joined to) an AD domain, run from an elevated session.
- Windows PowerShell 5.1 — the version that ships with Windows Server. The commands here also work under PowerShell 7 with the compatibility layer, but I'm writing for 5.1.
- The ActiveDirectory module is available (it is, on a DC; on a workstation install RSAT).
- Domain is
example.com; target container isOU=Staff,DC=example,DC=com. Replace both with yours.
The CSV
Keep the header row exactly as below. Save it UTF-8. One row per user.
FirstName,LastName,SamAccountName,Department,Title,Password
Jane,Doe,jdoe,Finance,Analyst,Chang3Me!Now22
Raj,Patel,rpatel,IT,Engineer,Sup3r$ecret_88
A few notes on the columns:
SamAccountNameis the pre-Windows-2000 logon name. It must be unique and 20 characters or fewer.Passwordmust satisfy your domain password policy orNew-ADUserwill reject the row — that's one of the errors the script catches and reports.
The script
Save this as New-UsersFromCsv.ps1. Read the comments; the only non-obvious lines are the UPN construction and the password conversion.
#Requires -Modules ActiveDirectory
# --- Settings you edit ---
$CsvPath = 'C:\path\to\new-users.csv' # your CSV
$TargetOU = 'OU=Staff,DC=example,DC=com' # where accounts land
$UpnSuffix = 'example.com' # UPN domain, e.g. jdoe@example.com
$LogPath = 'C:\path\to\onboarding-log.csv' # results written here
# -------------------------
Import-Module ActiveDirectory
$results = @()
foreach ($row in Import-Csv -Path $CsvPath) {
$sam = $row.SamAccountName.Trim()
try {
# Skip if the account already exists, so a re-run doesn't error out.
if (Get-ADUser -Filter "SamAccountName -eq '$sam'" -ErrorAction SilentlyContinue) {
throw "SamAccountName '$sam' already exists"
}
$securePw = ConvertTo-SecureString $row.Password -AsPlainText -Force
# Splat the parameters for readability.
$params = @{
Name = "$($row.FirstName) $($row.LastName)"
GivenName = $row.FirstName
Surname = $row.LastName
DisplayName = "$($row.FirstName) $($row.LastName)"
SamAccountName = $sam
UserPrincipalName = "$sam@$UpnSuffix"
Department = $row.Department
Title = $row.Title
Path = $TargetOU
AccountPassword = $securePw
Enabled = $true
ChangePasswordAtLogon = $true # force reset at first logon
ErrorAction = 'Stop' # so the catch block fires
}
New-ADUser @params
$results += [pscustomobject]@{
SamAccountName = $sam
Status = 'Created'
Detail = ''
}
Write-Host "Created $sam" -ForegroundColor Green
}
catch {
# One bad row is logged and we move on to the next.
$results += [pscustomobject]@{
SamAccountName = $sam
Status = 'Failed'
Detail = $_.Exception.Message
}
Write-Warning "Failed $sam : $($_.Exception.Message)"
}
}
$results | Export-Csv -Path $LogPath -NoTypeInformation
Write-Host "`nDone. Results written to $LogPath"
Why it's built this way
ErrorAction = 'Stop'inside the splat. By default manyNew-ADUserfailures are non-terminating and won't triggercatch. ForcingStopis what makes the per-row error handling actually work.- The existence check. Re-running the script is common — you fix three bad rows in the CSV and run it again. The
Get-ADUsercheck means the accounts that already succeeded are skipped and reported, not re-created or errored noisily. - A results log. Every row ends up as
CreatedorFailedwith the reason, so you have a record to hand back to whoever gave you the list.
Do a dry run first
Before creating anything, confirm your CSV parses and see exactly what would be created. Comment out the New-ADUser @params line and run it — you'll get the "Created" messages and a log without touching AD. Point it at a test OU with two rows for the first real run. I do this every time on an unfamiliar CSV.
Fields I left out on purpose — office, manager, group membership, home directory — are all real New-ADUser parameters or follow-on steps (Add-ADGroupMember for groups). I'm not going to guess your attribute names. If you want to add columns, check Microsoft Learn for New-ADUser for the exact parameter name and value type before you wire it in, rather than assuming.
Verify it worked
First, read the log the script wrote — that's your at-a-glance pass/fail:
Import-Csv 'C:\path\to\onboarding-log.csv' | Format-Table -AutoSize
Then confirm the objects actually exist in AD and landed in the right OU:
# List everything created in the target OU
Get-ADUser -Filter * -SearchBase 'OU=Staff,DC=example,DC=com' |
Select-Object SamAccountName, Enabled, UserPrincipalName |
Sort-Object SamAccountName
# Spot-check a single account
Get-ADUser -Identity jdoe -Properties Department, Title, UserPrincipalName
Check that Enabled is True and the UPN reads sam@example.com as expected.
Undo / rollback
If a test batch needs to disappear, delete the accounts you just created. Remove-ADUser is irreversible short of AD Recycle Bin or a restore, so filter tightly and use -WhatIf first to see exactly what would go.
Using the log the script produced (only the ones marked Created):
# DRY RUN: shows what would be removed, deletes nothing
Import-Csv 'C:\path\to\onboarding-log.csv' |
Where-Object Status -eq 'Created' |
ForEach-Object { Remove-ADUser -Identity $_.SamAccountName -WhatIf }
Inspect that output carefully. When you're certain, remove -WhatIf and add -Confirm:$false if you want to skip the per-object prompt. I leave the confirmation prompt on for anything that deletes accounts — a few extra keystrokes is cheap insurance.
Never delete "everything in the OU" as a rollback unless that OU was created solely for this test and holds nothing else. Scope your cleanup to the accounts you made.
For the specific parameters and their accepted values, see Microsoft Learn for New-ADUser, Get-ADUser, and Remove-ADUser, and for enabling the AD Recycle Bin if you don't already have it on.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
