ZZentralit
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&apos;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.
  • ChangePasswordAtLogon means the generated password is a single-use token.

Extensions

  • Add an -EmailTemplate parameter to send welcome mails via Send-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.