Systems Admin

Export Active Directory Users to CSV with PowerShell

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:

1. Read Get-ADUser scope filter: -Filter * -SearchBase $ouDN -SearchScope Subtree columns: -Properties Title,Mail,… | 2. Shape Select-Object pick + rename: DisplayName, Mail, Department, Title, Manager (computed), LastLogonDate, Enabled | 3. Write Export-Csv file path: -Path C:\Reports\ AD-Users.csv flags: -NoTypeInformation -Encoding UTF8 The 3-cmdlet pipeline: read → shape → write Each stage is independent • swap any one without rewriting the others
The conceptual recipe: 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:

  1. 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.
  2. From PowerShell. If you only know the OU’s name, look it up directly:
    Get-ADOrganizationalUnit -Filter "Name -eq 'Sales'" | Select-Object DistinguishedName

    For 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:

  • -NoTypeInformation suppresses the legacy #TYPE header row that older PowerShell versions wrote. Excel imports the file without it.
  • -Encoding UTF8 preserves 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.
Administrator: Windows PowerShell PS C:\Scripts> .\Export-ADUsers.ps1 -OuDN “OU=Users,DC=corp,DC=local” Querying directory… scope: OU=Users,DC=corp,DC=local (Subtree) properties: DisplayName, EmailAddress, Department, Title, Manager, … [OK] 4,226 users matched [OK] resolved 587 manager DNs to display names [OK] shaped output: 11 columns per row Writing CSV: C:\Reports\AD-Users-2026-05-06.csv [OK] wrote 4,226 rows in 11.4 s [OK] file size: 612 KB (UTF-8) PS C:\Scripts>
Sample run against a 4,200-user OU: about ten seconds end-to-end on a healthy DC. The bulk of the time is the Manager-DN-to-name lookups; pre-loading the manager set with a single 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:

  1. The default -Path includes the date so re-runs auto-version. You can override it for any specific run.
  2. The $Properties array 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

AD-Users-2026-05-06.csv • Microsoft Excel DisplayName EmailAddress Department Title Manager Status LastLogonDate Adrian Cooper a.cooper@corp.local Engineering Senior Developer Helena Roy Active 2026-05-05 09:14 Bina Patel b.patel@corp.local Sales Account Executive Joel Mensah Active 2026-05-06 07:42 Chen Nakamura c.nakamura@corp.local Engineering Engineering Manager Helena Roy Active 2026-05-06 08:01 Daria Okafor d.okafor@corp.local Marketing Content Lead Steven Vrana Active 2026-05-04 16:55 Erik Hartmann e.hartmann@corp.local Finance Controller Maya Singh Active 2026-05-06 06:30 Fernanda Reyes f.reyes@corp.local Support Tier 2 Engineer Joel Mensah Active 2026-05-05 22:11 Greta Lindqvist g.lindqvist@corp.local Engineering QA Engineer Chen Nakamura Disabled 2026-02-12 14:08 Hassan Yousef h.yousef@corp.local Engineering DevOps Engineer Chen Nakamura Active 2026-05-06 11:23 … 4,218 more rows below
The same export opened in Excel: column headers from 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 -LDAPFilter for tricky predicates. The -Filter language is convenient but limited; -LDAPFilter accepts the full RFC 4515 LDAP filter syntax. -LDAPFilter "(&(objectCategory=user)(memberOf=CN=VPN-Users,OU=Groups,DC=corp,DC=local))" is something -Filter cannot express directly.
  • Pre-resolve managers in one pass. Calling Get-ADUser inside 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 500 tells AD to return results in chunks; otherwise some DCs cap result sets around 1,000 entries. Pair with -ResultSetSize $null to 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).Count against 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:

  1. Which users?Get-ADUser with -Filter, -SearchBase, -SearchScope.
  2. Which columns?Select-Object with property names and computed properties.
  3. Which file?Export-Csv with -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.

Leave a Reply