Shore Up
A row of blank identity cards being stamped one by one and slotted into a filing cabinet, with a clipboard nearby holding a checklist of names.
WindowsSecurity

Batch-Create Active Directory Users from CSV with PowerShell

Ketan Aagja8 min read
No ratings yet

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-Test in 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 is OU=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:

  • SamAccountName is the pre-Windows-2000 logon name. It must be unique and 20 characters or fewer.
  • Password must satisfy your domain password policy or New-ADUser will 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 many New-ADUser failures are non-terminating and won't trigger catch. Forcing Stop is 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-ADUser check means the accounts that already succeeded are skipped and reported, not re-created or errored noisily.
  • A results log. Every row ends up as Created or Failed with 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.

Written by
Ketan Aagja

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.

Automate Active Directory User Creation from a CSV with PowerShell

This guide builds a PowerShell script that reads a CSV file of people and creates a matching Active Directory user account for each row — name, logon name, OU placement, and an initial password. Its purpose is to save you from clicking through Active Directory Users and Computers a hundred times when you onboard a class, a department, or a new office.

8 min read

Bulk-Update AD User Attributes from a CSV

This guide reads a CSV of users and writes two attributes back to Active Directory for each one: Title and Department . Nothing is created or deleted — existing user objects have those two fields overwritten with the values in your file. That overwrite is a real change: if the CSV has a wrong value in a row, that user's title or department is now wrong until you fix it. There is no built-in "undo," so the safety step below is to export the current values first so you can put them back.

9 min read

Update Active Directory User Details with PowerShell

This guide changes existing Active Directory user objects: it sets the email address, job title, and department on one account or a batch of accounts. It does not create or delete anything. But it overwrites whatever those attributes held before, and Active Directory keeps no built-in undo — if you set the wrong department on 200 people, the only way back is to set it again with the correct value. So treat the write as permanent unless you record the old values first (I show how below).

8 min read

Automate Moving AD Users Between OUs From a CSV

This procedure reads a CSV of user accounts and their destination OUs, then moves each account to its target OU with PowerShell's Move-ADObject . Moving a user changes its distinguished name (DN). That matters because anything scoped by DN or OU — Group Policy links, delegated permissions, and OU-based filters — will start or stop applying to the account the moment it moves. The move itself does not delete the account or its group memberships, and it is reversible if you know where the account came from — which is why the script below records the original OU of every user before it touches anything.

8 min read