Why Script the User Export?
The same question keeps coming back from compliance, finance, and the help desk: can you give me a list of every user in Active Directory, with these specific fields, in a CSV? A license auditor wants names, emails, departments, and last-logon dates. Finance wants every active employee mapped to a manager and a cost-center code. The service desk wants a snapshot of mobile numbers so they can verify caller identity before resetting passwords. The IAM team wants every account that has not signed in for 90 days, with the OU it lives in, so they can disable the stale ones.
You can answer all of these with the ADUC GUI — one user at a time. Or you can answer all of them with a three-cmdlet PowerShell pipeline that reads from AD, picks the columns the requester actually cares about, and writes the result to a CSV that opens cleanly in Excel. This article walks through that pipeline as a recipe you can extend rather than a one-shot script you copy and paste.
The Three-Cmdlet Recipe
Every “export AD users to CSV” script in the wild boils down to three cmdlets in a pipeline:
Get-ADUser reads the directory, Select-Object picks and renames columns, Export-Csv writes the file. Each stage is independent — swap any one without rewriting the others.Mentally separating the three stages is the difference between copying a script that mostly works and being able to adapt the export for any new request. Once you can answer the three questions “which users? which columns? which file?”, you can write the pipeline.
Step 1 — Choose Your Scope
The -Filter, -SearchBase, and -SearchScope parameters of Get-ADUser together describe which users you want. The four scoping patterns you will use 95% of the time:
| Goal | Pattern |
|---|---|
| Every user in the domain | Get-ADUser -Filter * |
| One specific OU only (no children) | Get-ADUser -Filter * -SearchBase $ou -SearchScope OneLevel |
| An OU and everything under it | Get-ADUser -Filter * -SearchBase $ou -SearchScope Subtree |
| An attribute-based slice | Get-ADUser -Filter "Department -eq 'Sales' -and Enabled -eq \$true" |
The fourth pattern — an attribute-based -Filter — is what makes scripted exports actually useful. “Every active engineer hired in the last six months” is a one-line filter; the same question is twenty minutes of clicking in ADUC.
Finding an OU’s distinguishedName
For -SearchBase you need the full distinguished name of the target OU. Two ways to get it:
- From ADUC. Open Active Directory Users and Computers, click View > Advanced Features (the menu must be ticked or the Attribute Editor tab will not appear). Right-click the OU, choose Properties, switch to the Attribute Editor tab, find
distinguishedName, double-click and copy the value. - From PowerShell. If you only know the OU’s name, look it up directly:
Get-ADOrganizationalUnit -Filter "Name -eq 'Sales'" | Select-Object DistinguishedNameFor an OU you know lives under a specific parent, scope the lookup the same way:
Get-ADOrganizationalUnit -Filter * -SearchBase "OU=Departments,DC=corp,DC=local" | Select-Object Name, DistinguishedName
Step 2 — Choose Your Properties
By default, Get-ADUser returns a small handful of properties (Name, SamAccountName, UPN, SID, Enabled, GivenName, Surname, ObjectClass, ObjectGUID, DistinguishedName). Most useful exports need more than that — the trick is asking for only what you need.
Three patterns:
-Properties *— pulls every property AD knows about for each user. Convenient for one-off exploration; expensive on large directories. Avoid in production scripts.-Properties prop1,prop2,prop3— the right answer for any reusable script. Asks for exactly the columns you will export and nothing else.- Default + computed columns — combine the default property set with calculated values you build inside
Select-Object(covered in the next section).
A property set that covers most real-world requests:
$props = @(
'GivenName','Surname','DisplayName',
'SamAccountName','UserPrincipalName',
'EmailAddress','Department','Title','Manager','Company',
'Office','OfficePhone','MobilePhone',
'StreetAddress','City','State','PostalCode','Country',
'Description','EmployeeID',
'Enabled','LockedOut','PasswordLastSet','LastLogonDate',
'whenCreated','DistinguishedName'
)
Save that array to a variable, then reuse it in -Properties $props and again in Select-Object $props — the script stays DRY and any property you add later flows through both stages.
Step 3 — Shape the Output with Select-Object
Select-Object does three jobs in one cmdlet: it picks columns, renames them, and lets you build new columns from existing ones using computed properties. That third job is what turns a raw AD dump into a report a non-admin can read.
The two computed properties you will use most often:
Select-Object DisplayName, EmailAddress, Department, Title,
@{N='Manager'; E={ if (\$_.Manager) { (Get-ADUser \$_.Manager).Name } }},
@{N='OU'; E={ ((\$_.DistinguishedName -split ',',2)[1]) }},
@{N='AccountStatus';E={ if (\$_.Enabled) { 'Active' } else { 'Disabled' } }},
LastLogonDate, whenCreated
The @{N='Name'; E={ … }} hashtable syntax (Name + Expression) is the universal way to add a column whose value is computed from the current pipeline object. The first one above turns a raw Manager distinguished name into the manager’s display name; the second strips the leading CN=…, off the user’s DN to produce a clean OU path; the third converts the Enabled boolean to human-readable text.
The One-Liner
Everything together as a single command you can paste into any DC’s elevated PowerShell session:
Get-ADUser -Filter * -SearchBase "OU=Users,DC=corp,DC=local" -SearchScope Subtree `
-Properties DisplayName,EmailAddress,Department,Title,Manager,Office,
MobilePhone,Enabled,LastLogonDate,whenCreated,DistinguishedName |
Select-Object DisplayName, EmailAddress, Department, Title,
@{N='Manager';E={ if (\$_.Manager) { (Get-ADUser \$_.Manager).Name } }},
Office, MobilePhone,
@{N='OU';E={ ((\$_.DistinguishedName -split ',',2)[1]) }},
@{N='Status';E={ if (\$_.Enabled) { 'Active' } else { 'Disabled' } }},
LastLogonDate, whenCreated |
Export-Csv -Path "C:\Reports\AD-Users-$(Get-Date -Format 'yyyy-MM-dd').csv" `
-NoTypeInformation -Encoding UTF8
Three flags on Export-Csv that are not optional in 2026:
-NoTypeInformationsuppresses the legacy#TYPEheader row that older PowerShell versions wrote. Excel imports the file without it.-Encoding UTF8preserves non-ASCII characters in names (umlauts, accents, ideographs). Without it, Excel will round-trip them as garbage.- Date-stamped filenames via
$(Get-Date -Format 'yyyy-MM-dd')mean re-running the script never overwrites yesterday’s export. The ISO date prefix also sorts correctly in File Explorer.
Get-ADUser -Filter * call cuts that roughly in half.Wrap It as a Reusable Function
The one-liner is fine for ad-hoc work. For anything you will run more than once, wrap it as a function with parameters — same pipeline, more reuse:
function Export-ADUsersToCsv {
[CmdletBinding()]
param(
[Parameter(Mandatory)] [string] \$OuDN,
[string] \$Path = "C:\Reports\AD-Users-\$(Get-Date -Format 'yyyy-MM-dd').csv",
[string[]] \$Properties = @(
'DisplayName','EmailAddress','Department','Title','Manager',
'Office','MobilePhone','Enabled','LastLogonDate','whenCreated','DistinguishedName'
)
)
\$users = Get-ADUser -Filter * -SearchBase \$OuDN -SearchScope Subtree -Properties \$Properties
\$users |
Select-Object DisplayName, EmailAddress, Department, Title,
@{N='Manager';E={ if (\$_.Manager) { (Get-ADUser \$_.Manager).Name } }},
Office, MobilePhone,
@{N='OU';E={ ((\$_.DistinguishedName -split ',',2)[1]) }},
@{N='Status';E={ if (\$_.Enabled) { 'Active' } else { 'Disabled' } }},
LastLogonDate, whenCreated |
Export-Csv -Path \$Path -NoTypeInformation -Encoding UTF8
Write-Host "Wrote \$(\$users.Count) rows to \$Path" -ForegroundColor Green
}
# Usage:
Export-ADUsersToCsv -OuDN "OU=Sales,OU=Users,DC=corp,DC=local"
Export-ADUsersToCsv -OuDN "OU=Engineering,OU=Users,DC=corp,DC=local" `
-Path "\\fileserver\reports\engineering-users.csv"
Two upgrades that are worth the line of code:
- The default
-Pathincludes the date so re-runs auto-version. You can override it for any specific run. - The
$Propertiesarray is a parameter, so callers can ask for a different column set without forking the function. The same function exports a 5-column report or a 25-column report depending on what the requester wanted.
Common Variations
Ninety percent of real-world requests are the basic export with one extra filter clause. A few you will write more than once:
Disabled accounts only
Get-ADUser -Filter "Enabled -eq \$false" -SearchBase \$ou -Properties \$props |
Select-Object DisplayName, SamAccountName, Department, LastLogonDate, whenCreated |
Export-Csv "C:\Reports\AD-Disabled.csv" -NoTypeInformation -Encoding UTF8
Stale accounts (no logon in 90 days)
\$cutoff = (Get-Date).AddDays(-90)
Get-ADUser -Filter "LastLogonDate -lt '\$cutoff' -and Enabled -eq \$true" `
-SearchBase \$ou -Properties LastLogonDate, Department, Manager, DistinguishedName |
Select-Object DisplayName, SamAccountName, LastLogonDate, Department,
@{N='OU';E={ ((\$_.DistinguishedName -split ',',2)[1]) }} |
Export-Csv "C:\Reports\AD-Stale-90d.csv" -NoTypeInformation -Encoding UTF8
Members of a specific group
Get-ADGroupMember -Identity "Domain Admins" -Recursive |
Where-Object { \$_.objectClass -eq 'user' } |
Get-ADUser -Properties EmailAddress, Title, Department, LastLogonDate |
Select-Object DisplayName, EmailAddress, Title, Department, LastLogonDate |
Export-Csv "C:\Reports\AD-DomainAdmins.csv" -NoTypeInformation -Encoding UTF8
The first cmdlet changes; the rest of the pipeline does not. That is the whole point of the three-cmdlet recipe: the shape and write stages are reusable across every export.
The CSV in Excel
Select-Object, one row per user, computed columns (Manager name, Status) sitting alongside raw fields. Filter, pivot, or pipe to a downstream system from here.Operational Tips
- Use
-LDAPFilterfor tricky predicates. The-Filterlanguage is convenient but limited;-LDAPFilteraccepts the full RFC 4515 LDAP filter syntax.-LDAPFilter "(&(objectCategory=user)(memberOf=CN=VPN-Users,OU=Groups,DC=corp,DC=local))"is something-Filtercannot express directly. - Pre-resolve managers in one pass. Calling
Get-ADUserinside a computed property runs one round trip per row. For 5,000 users that is 5,000 extra LDAP queries. For large directories, fetch the manager set once into a hashtable and look up by DN inside the computed property — ten seconds becomes one. - Page large result sets.
Get-ADUser -ResultPageSize 500tells AD to return results in chunks; otherwise some DCs cap result sets around 1,000 entries. Pair with-ResultSetSize $nullto disable the upper bound. - Always
-Encoding UTF8. Without it, every name with a character outside ASCII round-trips as a question mark. Excel reads UTF-8 CSVs natively as long as UTF-8 with BOM is what you wrote (the default in PowerShell 7). - Schedule the export. Wrap the function in a script, register it as a Task Scheduler job that runs nightly at 02:00, and write the output to
\\fileserver\reports\. The auditors will love you and the help desk gets a same-day-fresh phone book without filing a ticket. - Sanity-check row counts. Compare
(Get-ADUser -Filter * -SearchBase $ou -SearchScope Subtree | Measure-Object).Countagainst your CSV row count after the export. Drift between the two means the pipeline is silently dropping rows — usually a malformed computed property throwing per-row.
Conclusion
Three cmdlets, three questions:
- Which users? —
Get-ADUserwith-Filter,-SearchBase,-SearchScope. - Which columns? —
Select-Objectwith property names and computed properties. - Which file? —
Export-Csvwith-Path,-NoTypeInformation, and-Encoding UTF8.
Once the recipe is internalized, the answer to any “can you give me a CSV of users where…” ticket is a five-line edit, not a thirty-minute click-fest in ADUC. Wrap it as a reusable function, parameterize the OU and the property list, and the same script powers the audit report, the license reconciliation, the stale-account hunt, and the help-desk phone book.