POWERSHELL ACTIVE DIRECTORY AUTOMATION Data Sources CSV Files HR Database Service Desk PowerShell Scripts New-ADUser Set-ADUser Get-ADGroupMember Import-Module AD Domain Controller AD DS LDAP / Kerberos DNS / DHCP Group Policy User Accounts Create / Disable / Move Groups & OUs Membership Mgmt Reports CSV / HTML / Email Task Scheduler Daily user audit Weekly reports Account cleanup From CSV data to fully provisioned AD accounts — automated and auditable

Managing Active Directory manually through the GUI works for a handful of users, but it becomes unsustainable when you need to onboard 50 employees, audit thousands of accounts for compliance, or disable departed users across multiple OUs. PowerShell transforms Active Directory administration from a repetitive, error-prone chore into a fast, repeatable, and auditable process. Every action becomes a script you can review, test, and schedule.

This guide covers the essential PowerShell commands and scripts every Windows administrator needs for Active Directory automation — from querying users and managing groups to bulk provisioning from CSV files and generating compliance reports.


Prerequisites

Before you begin, ensure you have:

  • Windows Server 2016+ with Active Directory Domain Services role, or a workstation joined to the domain
  • RSAT Active Directory module installed
  • PowerShell 5.1 or later (PowerShell 7 also supports the AD module)
  • Appropriate AD permissions (Domain Admin for full operations, or delegated permissions for specific OUs)

Install the AD Module

On a Domain Controller:

Install-WindowsFeature RSAT-AD-PowerShell

On a Windows 10/11 workstation:

# PowerShell (elevated)
Add-WindowsCapability -Online -Name Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0

Verify the module is available:

Import-Module ActiveDirectory
Get-Module ActiveDirectory

Querying Active Directory

Find Users

The Get-ADUser cmdlet is your primary tool for querying user accounts:

# Get a specific user with all properties
Get-ADUser -Identity jsmith -Properties *

# Find all enabled users
Get-ADUser -Filter {Enabled -eq $true} -Properties DisplayName, EmailAddress |
    Select-Object SamAccountName, DisplayName, EmailAddress

# Find users in a specific OU
Get-ADUser -Filter * -SearchBase "OU=Sales,DC=contoso,DC=com" |
    Select-Object Name, SamAccountName

# Find users whose password is expiring within 7 days
$cutoff = (Get-Date).AddDays(7)
Get-ADUser -Filter {Enabled -eq $true -and PasswordNeverExpires -eq $false} `
    -Properties PasswordLastSet, msDS-UserPasswordExpiryTimeComputed |
    Where-Object { [datetime]::FromFileTime($_.'msDS-UserPasswordExpiryTimeComputed') -lt $cutoff } |
    Select-Object Name, SamAccountName, @{N='ExpiryDate';E={[datetime]::FromFileTime($_.'msDS-UserPasswordExpiryTimeComputed')}}

Find Inactive Accounts

Locate accounts that haven’t logged in for 90 days:

$inactiveDays = 90
$cutoffDate = (Get-Date).AddDays(-$inactiveDays)

Get-ADUser -Filter {LastLogonDate -lt $cutoffDate -and Enabled -eq $true} `
    -Properties LastLogonDate, Description, Department |
    Select-Object SamAccountName, Name, LastLogonDate, Department |
    Sort-Object LastLogonDate |
    Export-Csv -Path "C:\Reports\InactiveUsers.csv" -NoTypeInformation

Write-Host "Report saved. Found $(($report | Measure-Object).Count) inactive accounts."

Query Group Memberships

# List all members of a group
Get-ADGroupMember -Identity "Domain Admins" -Recursive |
    Select-Object Name, SamAccountName, objectClass

# Find all groups a user belongs to
Get-ADUser -Identity jsmith -Properties MemberOf |
    Select-Object -ExpandProperty MemberOf |
    ForEach-Object { (Get-ADGroup $_).Name }

# Find empty groups
Get-ADGroup -Filter * -Properties Members |
    Where-Object { $_.Members.Count -eq 0 } |
    Select-Object Name, GroupScope, GroupCategory

Automating User Provisioning

Create a Single User

$password = ConvertTo-SecureString "T3mp@P@ss2026!" -AsPlainText -Force

New-ADUser -Name "Jane Smith" `
    -GivenName "Jane" `
    -Surname "Smith" `
    -SamAccountName "janesmith" `
    -UserPrincipalName "janesmith@contoso.com" `
    -Path "OU=Sales,OU=Users,DC=contoso,DC=com" `
    -AccountPassword $password `
    -ChangePasswordAtLogon $true `
    -Enabled $true `
    -Department "Sales" `
    -Title "Account Manager" `
    -Office "Building A" `
    -EmailAddress "janesmith@contoso.com"

Bulk User Creation from CSV

Create a CSV file (new-users.csv):

FirstName,LastName,Username,Department,Title,OU,Groups
Jane,Smith,jsmith,Sales,Account Manager,"OU=Sales,OU=Users,DC=contoso,DC=com","Sales Team;VPN Users"
Bob,Johnson,bjohnson,Engineering,Developer,"OU=Engineering,OU=Users,DC=contoso,DC=com","Dev Team;VPN Users;GitHub Access"
Maria,Garcia,mgarcia,HR,HR Analyst,"OU=HR,OU=Users,DC=contoso,DC=com","HR Team;Payroll Readers"

The provisioning script:

# Bulk-Create-ADUsers.ps1
param(
    [Parameter(Mandatory=$true)]
    [string]$CsvPath,

    [string]$DefaultPassword = "Welcome@2026!",
    [string]$LogPath = "C:\Logs\UserCreation_$(Get-Date -Format 'yyyyMMdd_HHmmss').log"
)

$ErrorActionPreference = "Continue"
Import-Module ActiveDirectory

$securePassword = ConvertTo-SecureString $DefaultPassword -AsPlainText -Force
$users = Import-Csv -Path $CsvPath
$created = 0
$failed = 0

foreach ($user in $users) {
    $upn = "$($user.Username)@contoso.com"
    $displayName = "$($user.FirstName) $($user.LastName)"

    try {
        # Check if user already exists
        if (Get-ADUser -Filter {SamAccountName -eq $($user.Username)} -ErrorAction SilentlyContinue) {
            $msg = "SKIP: User $($user.Username) already exists"
            Write-Warning $msg
            Add-Content -Path $LogPath -Value "$(Get-Date) - $msg"
            continue
        }

        # Create the user
        New-ADUser -Name $displayName `
            -GivenName $user.FirstName `
            -Surname $user.LastName `
            -SamAccountName $user.Username `
            -UserPrincipalName $upn `
            -Path $user.OU `
            -AccountPassword $securePassword `
            -ChangePasswordAtLogon $true `
            -Enabled $true `
            -Department $user.Department `
            -Title $user.Title `
            -EmailAddress "$($user.Username)@contoso.com"

        # Add to groups
        if ($user.Groups) {
            $groups = $user.Groups -split ";"
            foreach ($group in $groups) {
                Add-ADGroupMember -Identity $group.Trim() -Members $user.Username
            }
        }

        $msg = "OK: Created $($user.Username) in $($user.OU)"
        Write-Host $msg -ForegroundColor Green
        Add-Content -Path $LogPath -Value "$(Get-Date) - $msg"
        $created++
    }
    catch {
        $msg = "FAIL: $($user.Username) - $($_.Exception.Message)"
        Write-Host $msg -ForegroundColor Red
        Add-Content -Path $LogPath -Value "$(Get-Date) - $msg"
        $failed++
    }
}

Write-Host "`nResults: $created created, $failed failed. Log: $LogPath"

Run the script:

.\Bulk-Create-ADUsers.ps1 -CsvPath "C:\Data\new-users.csv"

Bulk Modifications

Update User Attributes

# Update department for all users in an OU
Get-ADUser -Filter * -SearchBase "OU=Sales,DC=contoso,DC=com" |
    Set-ADUser -Department "Sales & Marketing" -Company "Contoso Ltd"

# Set manager for a team
$manager = (Get-ADUser -Identity "manager01").DistinguishedName
Get-ADUser -Filter {Department -eq "Engineering"} |
    Set-ADUser -Manager $manager

# Move users between OUs
Get-ADUser -Filter {Department -eq "Sales"} -SearchBase "OU=OldSales,DC=contoso,DC=com" |
    Move-ADObject -TargetPath "OU=Sales,OU=Users,DC=contoso,DC=com"

Disable Departed Users

# Disable-DepartedUsers.ps1
param(
    [Parameter(Mandatory=$true)]
    [string]$CsvPath  # CSV with a "Username" column
)

$disabledOU = "OU=Disabled Users,DC=contoso,DC=com"
$users = Import-Csv -Path $CsvPath

foreach ($user in $users) {
    try {
        $adUser = Get-ADUser -Identity $user.Username -Properties MemberOf

        # Disable the account
        Disable-ADAccount -Identity $user.Username

        # Remove from all groups except Domain Users
        $adUser.MemberOf | ForEach-Object {
            Remove-ADGroupMember -Identity $_ -Members $user.Username -Confirm:$false
        }

        # Add a description with the disable date
        Set-ADUser -Identity $user.Username `
            -Description "Disabled $(Get-Date -Format 'yyyy-MM-dd') - Departed"

        # Move to Disabled Users OU
        $adUser | Move-ADObject -TargetPath $disabledOU

        Write-Host "Disabled: $($user.Username)" -ForegroundColor Yellow
    }
    catch {
        Write-Host "Error disabling $($user.Username): $($_.Exception.Message)" -ForegroundColor Red
    }
}

Reporting Scripts

Active Directory Health Report

# AD-HealthReport.ps1
$domain = Get-ADDomain
$report = @()

# Domain info
Write-Host "Domain: $($domain.DNSRoot)" -ForegroundColor Cyan
Write-Host "Forest: $($domain.Forest)"
Write-Host "Domain Controllers: $(($domain.ReplicaDirectoryServers | Measure-Object).Count)"

# User statistics
$allUsers = Get-ADUser -Filter * -Properties Enabled, LastLogonDate, PasswordExpired, PasswordNeverExpires
$totalUsers = ($allUsers | Measure-Object).Count
$enabledUsers = ($allUsers | Where-Object { $_.Enabled -eq $true } | Measure-Object).Count
$disabledUsers = $totalUsers - $enabledUsers
$expiredPasswords = ($allUsers | Where-Object { $_.PasswordExpired -eq $true -and $_.Enabled -eq $true } | Measure-Object).Count
$neverExpire = ($allUsers | Where-Object { $_.PasswordNeverExpires -eq $true -and $_.Enabled -eq $true } | Measure-Object).Count

# Build HTML report
$html = @"
<html>
<head><style>
body { font-family: Segoe UI, sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%; }
th { background: #0078D4; color: white; padding: 8px; text-align: left; }
td { border: 1px solid #ddd; padding: 8px; }
tr:nth-child(even) { background: #f2f2f2; }
.warn { color: #d83b01; font-weight: bold; }
</style></head>
<body>
<h1>Active Directory Health Report</h1>
<p>Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm')</p>
<h2>User Statistics</h2>
<table>
<tr><th>Metric</th><th>Count</th></tr>
<tr><td>Total Users</td><td>$totalUsers</td></tr>
<tr><td>Enabled</td><td>$enabledUsers</td></tr>
<tr><td>Disabled</td><td>$disabledUsers</td></tr>
<tr><td class="warn">Expired Passwords (Active)</td><td class="warn">$expiredPasswords</td></tr>
<tr><td class="warn">Password Never Expires</td><td class="warn">$neverExpire</td></tr>
</table>
</body></html>
"@

$html | Out-File "C:\Reports\AD-Health_$(Get-Date -Format 'yyyyMMdd').html"
Write-Host "Report saved to C:\Reports\" -ForegroundColor Green

Group Membership Audit

# Audit-PrivilegedGroups.ps1
$privilegedGroups = @(
    "Domain Admins",
    "Enterprise Admins",
    "Schema Admins",
    "Administrators",
    "Account Operators",
    "Backup Operators"
)

$results = foreach ($group in $privilegedGroups) {
    $members = Get-ADGroupMember -Identity $group -Recursive -ErrorAction SilentlyContinue
    foreach ($member in $members) {
        [PSCustomObject]@{
            Group       = $group
            Name        = $member.Name
            Account     = $member.SamAccountName
            ObjectClass = $member.objectClass
        }
    }
}

$results | Format-Table -AutoSize
$results | Export-Csv -Path "C:\Reports\PrivilegedGroups_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation

Scheduling AD Scripts

Create a Scheduled Task

Automate your scripts to run on a schedule:

$action = New-ScheduledTaskAction `
    -Execute "powershell.exe" `
    -Argument "-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\AD-HealthReport.ps1"

$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At "07:00"

$settings = New-ScheduledTaskSettingsSet `
    -StartWhenAvailable `
    -DontStopOnIdleEnd `
    -RestartCount 3 `
    -RestartInterval (New-TimeSpan -Minutes 5)

Register-ScheduledTask `
    -TaskName "AD Weekly Health Report" `
    -Action $action `
    -Trigger $trigger `
    -Settings $settings `
    -RunLevel Highest `
    -User "CONTOSO\svc-adreports" `
    -Password "ServiceAccountP@ss!"

Troubleshooting Common Issues

”Unable to find a default server with Active Directory Web Services running”

This error means PowerShell cannot reach a domain controller:

# Test connectivity to DCs
Test-Connection -ComputerName DC01 -Count 2

# Specify a DC directly
Get-ADUser -Identity jsmith -Server "DC01.contoso.com"

# Check AD Web Services
Get-Service ADWS -ComputerName DC01

Slow Queries on Large Domains

# Bad: retrieves ALL properties for ALL users
Get-ADUser -Filter * -Properties *  # Slow!

# Good: retrieve only what you need
Get-ADUser -Filter {Department -eq "Sales"} -Properties EmailAddress, LastLogonDate `
    -ResultSetSize 1000

# Use -SearchBase to limit scope
Get-ADUser -Filter * -SearchBase "OU=Sales,DC=contoso,DC=com" -SearchScope Subtree

Permission Errors

# Check your current permissions
whoami /groups

# Test if you can read an OU
Get-ADOrganizationalUnit -Identity "OU=Sales,DC=contoso,DC=com" -ErrorAction Stop

# Run as a different credential
$cred = Get-Credential
Get-ADUser -Filter * -Credential $cred

PowerShell AD Quick Reference

CmdletDescription
Get-ADUserQuery user accounts with filters
New-ADUserCreate a new user account
Set-ADUserModify user attributes
Remove-ADUserDelete a user account
Enable-ADAccountEnable a disabled account
Disable-ADAccountDisable an account
Unlock-ADAccountUnlock a locked-out account
Get-ADGroupQuery group objects
Add-ADGroupMemberAdd members to a group
Remove-ADGroupMemberRemove members from a group
Move-ADObjectMove objects between OUs
Search-ADAccountFind locked, expired, or inactive accounts

Summary

PowerShell is the most powerful tool available for managing Active Directory at scale. You’ve learned how to query users and groups efficiently, automate user provisioning from CSV files with proper error handling, perform bulk modifications across OUs, generate HTML health reports, audit privileged group memberships, and schedule scripts for ongoing automation.

Build a library of tested scripts in a shared repository, log every automated action for audit trails, and always test against a small subset before running bulk operations in production. These practices transform AD management from reactive firefighting into proactive, documented infrastructure management.