Short answer: run .\Export-ADUserReport.ps1 -ExportCsv C:\Reports\ad-users.csv on a machine with the ActiveDirectory module. You get one row per user with name, UPN, sAMAccountName, enabled state, last logon date, password last set, PasswordNeverExpires, manager, department, title, mail, created date and OU. Add -Enabled or -Disabled to filter, -SearchBase to limit it to one OU, -Properties for extra attributes and -ExportHtml for a page you can mail to a manager.
We ran this script on 6 October 2026 on a Windows Server 2025 Standard domain controller (build 26100.33438, September 2026 update, Windows PowerShell 5.1) and a Windows 11 Pro member PC in our lab domain contoso.com. The example output below is from that run; the lab domain is small and new, so your numbers will be bigger. It was also syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25.
Table of Contents
What it does
Most “export AD users” requests are the same: HR wants a list with managers and departments, an auditor wants enabled accounts with password ages, or you need a clean CSV before a migration. Get-ADUser -Filter * | Export-Csv gets you halfway, but the raw attributes come out as file-time numbers and full distinguished names that nobody outside IT can read.
This script does the conversions for you. It is read-only: it only queries Active Directory and writes the files you ask for.
lastLogonTimestampandpwdLastSetare turned into normal dates (empty if never set).PasswordNeverExpiresis read from theuserAccountControlflag 0x10000 (DONT_EXPIRE_PASSWORD).- The manager is shown as a name (“Alice Smith”), not a DN, and the OU column shows the container the account lives in.
- Enabled/disabled filtering is done by the domain controller with an LDAP bitwise filter, so it is fast on large domains.
- Extra attributes (
employeeID,physicalDeliveryOfficeName,extensionAttribute1…) become extra columns; multi-valued ones are joined with “; “. - Output to CSV (UTF-8, opens cleanly in Excel), a simple HTML table, the pipeline (
-PassThru) or the screen.
Requirements
- Windows PowerShell 5.1 or PowerShell 7 on Windows.
- The ActiveDirectory module (RSAT AD DS tools, or run on a domain controller).
- Read access to the user objects. Any domain user can read these attributes by default unless you have tightened permissions.
- No admin rights needed for the export itself, only write access to the folder you export to.
Download and first run
- Copy the script from the box on this page (or use the download button) and save it as
C:\Scripts\Export-ADUserReport.ps1. - If you downloaded the file, remove the “downloaded from the internet” mark so the execution policy lets it run:
Unblock-File C:\Scripts\Export-ADUserReport.ps1. - Run it on a domain-joined machine that has the ActiveDirectory module: a domain controller, a management server, or a Windows 11 PC with RSAT (see Install RSAT on Windows 11).
- Read the built-in help once, then run it with no options to see the result on screen before you export anything.
cd C:\Scripts
Get-Help .\Export-ADUserReport.ps1 -Full
.\Export-ADUserReport.ps1 -Enabled
.\Export-ADUserReport.ps1 -ExportCsv C:\Reports\ad-users.csv
Options
| Parameter | What it does | Default |
|---|---|---|
-SearchBase | DN of the OU or container to search, e.g. OU=Staff,DC=contoso,DC=com | Whole domain |
-SearchScope | Base, OneLevel or Subtree | Subtree |
-Enabled | Only enabled accounts | Off |
-Disabled | Only disabled accounts (cannot be combined with -Enabled) | Off |
-Properties | Extra LDAP attribute names to add as columns | None |
-Server | Domain controller or domain to query | A DC of your domain |
-Credential | Alternate credentials for the query | Current user |
-ExportCsv | Write a CSV file | Not written |
-ExportHtml | Write an HTML table | Not written |
-PassThru | Send the row objects down the pipeline | Off |
Usage examples
# Everyone, to CSV
.\Export-ADUserReport.ps1 -ExportCsv C:\Reports\ad-users.csv
# Enabled users in one OU, as an HTML page for a department head
.\Export-ADUserReport.ps1 -SearchBase "OU=Sales,DC=contoso,DC=com" -Enabled -ExportHtml C:\Reports\sales.html
# Disabled accounts with employee ID and description, for an HR reconciliation
.\Export-ADUserReport.ps1 -Disabled -Properties employeeID,description -ExportCsv C:\Reports\disabled.csv
# Enabled users with no manager set
.\Export-ADUserReport.ps1 -Enabled -PassThru | Where-Object { -not $_.Manager } | Format-Table Name, Department
# Passwords that never expire, oldest first
.\Export-ADUserReport.ps1 -Enabled -PassThru | Where-Object PasswordNeverExpires | Sort-Object PasswordLastSet
CSV columns
We have not published sample output because the script has not run against a real domain yet. These are the columns you get, in order:
| Column | Source | Notes |
|---|---|---|
| Name, DisplayName | name, displayName | |
| SamAccountName, UserPrincipalName | Default Get-ADUser properties | Pre-Windows 2000 logon name and UPN |
| Enabled | Get-ADUser Enabled | True / False |
| LastLogonDate | lastLogonTimestamp | Can be 9 to 14 days behind; empty if never logged on |
| PasswordLastSet | pwdLastSet | Empty if 0 (user must change password at next logon) |
| PasswordNeverExpires | userAccountControl 0x10000 | True / False |
| Manager | manager | Name taken from the manager’s DN |
| Department, Title, Mail | department, title, mail | |
| Created | whenCreated | |
| OU | Parent of the DN | e.g. OU=Staff,DC=contoso,DC=com |
| DistinguishedName | DN | Useful as a unique key |
| (your extras) | -Properties | One column per attribute, in the order given |
Example output
Run against the lab OU with -SearchBase 'OU=Lab,DC=contoso,DC=com' -ExportCsv C:\srvs-lab\out\ad-users.csv -ExportHtml C:\srvs-lab\out\ad-users.html. First three rows of the CSV:
8 user(s) written to C:\srvs-lab\out\ad-users.csv
HTML report written to C:\srvs-lab\out\ad-users.html
Name SamAccountName UserPrincipalName Enabled LastLogonDate PasswordLastSet PasswordNeverExpires Manager Department
---- -------------- ----------------- ------- ------------- --------------- -------------------- ------- ----------
Alice Jones alice alice@contoso.com True 10/6/2026 2:17:12 PM False IT
Backup Service svc-backup svc-backup@contoso.com True 10/6/2026 2:17:13 PM False IT
Bob Smith bob bob@contoso.com True 10/6/2026 2:17:12 PM False Alice Jones IT
LastLogonDate is empty for accounts that have never signed in, which is normal for the new test accounts.
Schedule it
Run it as a scheduled task so the report is waiting for you. A group managed service account (gMSA) is the cleanest identity for this: no password to store and nothing to expire. See our gMSA guide to create one and allow this server to retrieve its password.
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
-Argument '-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Export-ADUserReport.ps1 -Enabled -ExportCsv C:\Reports\ad-users.csv'
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 7am
$principal = New-ScheduledTaskPrincipal -UserId 'CONTOSO\gmsa-adreport$' -LogonType Password
Register-ScheduledTask -TaskName 'AD user export' -Action $action -Trigger $trigger -Principal $principal
The file is overwritten on every run. If you need history, point the task at a small wrapper script that adds the date to the file name, or copy the file away after each run.
How it works
- Checks that the ActiveDirectory module is installed and loads it.
- Builds an LDAP filter:
(&(objectCategory=person)(objectClass=user)), plus(!(userAccountControl:1.2.840.113556.1.4.803:=2))for enabled or(userAccountControl:1.2.840.113556.1.4.803:=2)for disabled accounts. The long number is the LDAP “bitwise AND” matching rule; bit 2 is ACCOUNTDISABLE. - Calls
Get-ADUser -LDAPFilter ... -Properties ...with a page size of 500, asking only for the attributes it needs, which keeps the query light. - Converts the file-time values with
[DateTime]::FromFileTime(), treating 0 and the “never” value as empty. - Writes the rows to the formats you chose. Nothing is written back to AD.
Limitations
- LastLogonDate is approximate.
lastLogonTimestampis replicated but, with default settings, lags the real last logon by 9 to 14 days. That is fine for finding stale accounts; it is not proof of when someone last signed in. The exact per-DClastLogonvalue is not replicated, and this script does not query every DC for it. - One domain at a time. Use
-Serverwith another domain’s name to export it separately. - Contacts and inetOrgPerson. The filter returns user objects of category person. Contacts are not included.
- Large exports. 50,000 users with many extra attributes produce a big file; filter with
-SearchBaseif Excel struggles. - Not yet run against a live domain (see the note at the top).
Official documentation: Get-ADUser (Microsoft Learn) · UserAccountControl flags · The lastLogonTimestamp attribute (Microsoft AskDS)
Related: Get-ADUser PowerShell Examples: 25 Queries for Active Directory · Find Inactive AD Users and Computers: PowerShell Cleanup in 5 Steps · Bulk Create AD Users from CSV: PowerShell Script in 7 Steps · AD Password Expiry Email: Reliable PowerShell Reminder Script in 6 Steps · Install RSAT on Windows 11: 5 Methods, Including Offline
See also: Locked Out AD Users Report: PowerShell Script with Lockout Source · AD Privileged Group Report: Domain Admins and adminCount Audit · AD Nested Group Membership: PowerShell Tree with Loop Detection · Inactive AD Accounts Report: PowerShell Script with Safe Disable · AD Health Check Report: dcdiag and repadmin PowerShell Script
The script
# Export AD Users to CSV: PowerShell Script with Last Logon (v1.0.0) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/export-ad-users-csv/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
<#
.SYNOPSIS
Export Active Directory users with the attributes admins actually ask for, to CSV and/or HTML.
.DESCRIPTION
Read-only. Queries user objects with Get-ADUser and writes one row per user:
Name, DisplayName, SamAccountName, UserPrincipalName, Enabled, LastLogonDate (from lastLogonTimestamp),
PasswordLastSet (from pwdLastSet), PasswordNeverExpires (userAccountControl flag 0x10000), Manager,
Department, Title, Mail, Created (whenCreated), OU (parent container) and DistinguishedName.
Any extra LDAP attributes passed with -Properties are added as columns after those.
lastLogonTimestamp is replicated, but by design it can be 9-14 days behind the real last logon.
Use it to spot stale accounts, not to prove when someone last signed in.
.PARAMETER SearchBase
Distinguished name of the OU or container to search, e.g. "OU=Staff,DC=contoso,DC=com". Default: whole domain.
.PARAMETER SearchScope
Base, OneLevel or Subtree (default Subtree).
.PARAMETER Enabled
Only enabled accounts.
.PARAMETER Disabled
Only disabled accounts.
.PARAMETER Properties
Extra LDAP attribute names to add as columns, e.g. employeeID, physicalDeliveryOfficeName, extensionAttribute1.
Multi-valued attributes are joined with "; ".
.PARAMETER Server
Domain controller or domain to query (default: a DC of the current domain).
.PARAMETER Credential
Alternate credentials for the AD query.
.PARAMETER ExportCsv
Write the report to this CSV file (UTF-8).
.PARAMETER ExportHtml
Write the report to this HTML file.
.PARAMETER PassThru
Output the row objects to the pipeline.
.EXAMPLE
.\Export-ADUserReport.ps1 -ExportCsv C:\Reports\ad-users.csv
.EXAMPLE
.\Export-ADUserReport.ps1 -SearchBase "OU=Sales,DC=contoso,DC=com" -Enabled -ExportHtml C:\Reports\sales.html
.EXAMPLE
.\Export-ADUserReport.ps1 -Disabled -Properties employeeID,description -ExportCsv C:\Reports\disabled.csv
.EXAMPLE
.\Export-ADUserReport.ps1 -Enabled -PassThru | Where-Object { -not $_.Manager } | Format-Table Name,Department
.NOTES
Name: Export-ADUserReport.ps1
Version: 1.0.0
Source: https://srvscripts.com/scripts/export-ad-users-csv/
License: MIT
Requires: Windows PowerShell 5.1 or PowerShell 7 on Windows, ActiveDirectory module (RSAT), read access to AD.
#>
[CmdletBinding(DefaultParameterSetName = 'All')]
param(
[ValidateNotNullOrEmpty()]
[string]$SearchBase,
[ValidateSet('Base', 'OneLevel', 'Subtree')]
[string]$SearchScope = 'Subtree',
[Parameter(ParameterSetName = 'Enabled')]
[switch]$Enabled,
[Parameter(ParameterSetName = 'Disabled')]
[switch]$Disabled,
[ValidatePattern('^[A-Za-z][A-Za-z0-9-]*$')]
[string[]]$Properties,
[ValidateNotNullOrEmpty()]
[string]$Server,
[System.Management.Automation.PSCredential]
[System.Management.Automation.Credential()]
$Credential = [System.Management.Automation.PSCredential]::Empty,
[ValidateNotNullOrEmpty()]
[string]$ExportCsv,
[ValidateNotNullOrEmpty()]
[string]$ExportHtml,
[switch]$PassThru
)
Set-StrictMode -Version 2
$ErrorActionPreference = 'Stop'
if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
throw "The ActiveDirectory module is not installed. Install RSAT: Active Directory Domain Services tools, then run again."
}
Import-Module ActiveDirectory -Verbose:$false
if (-not $ExportCsv -and -not $ExportHtml -and -not $PassThru) {
Write-Verbose 'No -ExportCsv, -ExportHtml or -PassThru given: results are shown on screen only.'
}
# ---- Helpers --------------------------------------------------------------------------------------------
function ConvertFrom-FileTimeValue {
param($Value)
if ($null -eq $Value) { return $null }
$v = [int64]$Value
if ($v -le 0 -or $v -eq [int64]::MaxValue) { return $null }
[DateTime]::FromFileTime($v)
}
function Get-RdnValue {
param([string]$DistinguishedName)
if (-not $DistinguishedName) { return '' }
if ($DistinguishedName -match '^(?:CN|OU)=((?:\\,|[^,])+)') { return ($Matches[1] -replace '\\,', ',') }
$DistinguishedName
}
function Get-ParentPath {
param([string]$DistinguishedName)
# Strip the first RDN, honouring escaped commas.
if ($DistinguishedName -match '^(?:\\,|[^,])+,(.+)$') { return $Matches[1] }
''
}
function Get-AttributeValue {
param($Entity, [string]$Name)
if ($Entity.PropertyNames -contains $Name) { return $Entity[$Name].Value }
$null
}
function Format-AttributeValue {
param($Value)
if ($null -eq $Value) { return '' }
if ($Value -is [System.Collections.ICollection] -and $Value -isnot [string] -and $Value -isnot [byte[]]) {
return (@($Value) | ForEach-Object { [string]$_ }) -join '; '
}
if ($Value -is [byte[]]) { return [Convert]::ToBase64String($Value) }
[string]$Value
}
# ---- Query ----------------------------------------------------------------------------------------------
$baseAttrs = 'displayName', 'mail', 'manager', 'department', 'title', 'whenCreated', 'lastLogonTimestamp', 'pwdLastSet', 'userAccountControl'
$extra = @()
if ($Properties) { $extra = @($Properties | Where-Object { $baseAttrs -notcontains $_ } | Select-Object -Unique) }
$ldap = '(&(objectCategory=person)(objectClass=user)'
if ($Enabled) { $ldap += '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' }
if ($Disabled) { $ldap += '(userAccountControl:1.2.840.113556.1.4.803:=2)' }
$ldap += ')'
$query = @{
LDAPFilter = $ldap
Properties = @($baseAttrs + $extra)
SearchScope = $SearchScope
ResultPageSize = 500
}
if ($SearchBase) { $query.SearchBase = $SearchBase }
if ($Server) { $query.Server = $Server }
if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { $query.Credential = $Credential }
Write-Verbose "LDAP filter: $ldap"
try {
$users = @(Get-ADUser @query)
} catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException] {
throw "SearchBase not found: '$SearchBase'. Check the distinguished name (Get-ADOrganizationalUnit -Filter * | Select DistinguishedName)."
} catch {
if ($_.Exception.Message -match 'attribute|property') {
throw "AD rejected the query. Check the names given to -Properties (LDAP display names such as employeeID). Details: $($_.Exception.Message)"
}
throw
}
# ---- Build rows -----------------------------------------------------------------------------------------
$rows = foreach ($u in $users) {
$uac = 0
$uacValue = Get-AttributeValue $u 'userAccountControl'
if ($null -ne $uacValue) { $uac = [int]$uacValue }
$row = [ordered]@{
Name = $u.Name
DisplayName = Format-AttributeValue (Get-AttributeValue $u 'displayName')
SamAccountName = $u.SamAccountName
UserPrincipalName = $u.UserPrincipalName
Enabled = $u.Enabled
LastLogonDate = ConvertFrom-FileTimeValue (Get-AttributeValue $u 'lastLogonTimestamp')
PasswordLastSet = ConvertFrom-FileTimeValue (Get-AttributeValue $u 'pwdLastSet')
PasswordNeverExpires = [bool]($uac -band 0x10000)
Manager = Get-RdnValue (Format-AttributeValue (Get-AttributeValue $u 'manager'))
Department = Format-AttributeValue (Get-AttributeValue $u 'department')
Title = Format-AttributeValue (Get-AttributeValue $u 'title')
Mail = Format-AttributeValue (Get-AttributeValue $u 'mail')
Created = (Get-AttributeValue $u 'whenCreated')
OU = Get-ParentPath $u.DistinguishedName
DistinguishedName = $u.DistinguishedName
}
foreach ($a in $extra) {
$row[$a] = Format-AttributeValue (Get-AttributeValue $u $a)
}
New-Object -TypeName psobject -Property $row
}
$rows = @($rows | Sort-Object Name)
# ---- Output ---------------------------------------------------------------------------------------------
if ($ExportCsv) {
$rows | Export-Csv -LiteralPath $ExportCsv -NoTypeInformation -Encoding UTF8
Write-Information ("{0} user(s) written to {1}" -f $rows.Count, $ExportCsv) -InformationAction Continue
}
if ($ExportHtml) {
$css = 'body{font-family:Segoe UI,Arial,sans-serif;font-size:13px;margin:20px}table{border-collapse:collapse}' +
'th,td{border:1px solid #ccc;padding:4px 8px;text-align:left}th{background:#f0f0f0}tr:nth-child(even){background:#fafafa}'
$title = 'AD user report'
$pre = "<h1>$title</h1><p>Generated {0:yyyy-MM-dd HH:mm} on {1}. {2} user(s).</p>" -f (Get-Date), $env:COMPUTERNAME, $rows.Count
$rows | ConvertTo-Html -Title $title -Head "<style>$css</style>" -PreContent $pre | Out-File -LiteralPath $ExportHtml -Encoding utf8
Write-Information ("HTML report written to {0}" -f $ExportHtml) -InformationAction Continue
}
if ($PassThru) { return $rows }
if (-not $ExportCsv -and -not $ExportHtml) {
if (-not $rows.Count) { Write-Information 'No users matched.' -InformationAction Continue; return }
$rows | Format-Table Name, SamAccountName, Enabled, LastLogonDate, PasswordLastSet, Department, Title -AutoSize
}
730c89665413a60bd851dfaf31ca6b19fec3f2d6dbbae90922241b8049abdfa3curl -fsSL -o Export-ADUserReport.ps1 https://scr.srvscripts.com/export-ad-users-csv/Export-ADUserReport.ps1 && curl -fsSL https://scr.srvscripts.com/export-ad-users-csv/Export-ADUserReport.ps1.sha256 | sha256sum -cInvoke-WebRequest -Uri 'https://scr.srvscripts.com/export-ad-users-csv/Export-ADUserReport.ps1' -OutFile 'Export-ADUserReport.ps1'; if ((Get-FileHash 'Export-ADUserReport.ps1' -Algorithm SHA256).Hash -eq '730C89665413A60BD851DFAF31CA6B19FEC3F2D6DBBAE90922241B8049ABDFA3') { 'OK: the file is intact' } else { 'MISMATCH: do not run this file' }Copy the whole line. In Windows PowerShell, curl and sha256sum are not the Linux tools, so use the PowerShell line there.Also on GitHub: github.com/srvscripts/scripts
Frequently asked questions
How do I export all AD users to CSV with PowerShell?
Run Export-ADUserReport.ps1 -ExportCsv C:\Reports\ad-users.csv on a machine with the ActiveDirectory module. Without the script, Get-ADUser -Filter * -Properties * | Export-Csv works but gives raw file-time numbers and DNs.
Why is LastLogonDate different from what the user says?
It comes from lastLogonTimestamp, which domain controllers only update when the stored value is older than about 9 to 14 days. It is designed for finding inactive accounts, not for exact logon times.
Can I export only one OU?
Yes. Pass the OU distinguished name to -SearchBase, for example -SearchBase “OU=Sales,DC=contoso,DC=com”. Add -SearchScope OneLevel to skip sub-OUs.
How do I add employeeID or other attributes?
Use -Properties with the LDAP display names, for example -Properties employeeID,physicalDeliveryOfficeName. Each one becomes a column at the end of the CSV.
Does the script change anything in Active Directory?
No. It only reads user objects and writes the CSV or HTML file you ask for.
Why does the CSV show strange characters in Excel?
The file is UTF-8. Open it with Data, From Text/CSV in Excel and choose UTF-8 if names with accents look wrong.