- Published on
How to Get a List of All OUs in Active Directory (PowerShell Script)
OU structures accumulate cruft the same way everything else in AD does — a folder created for a reorg that never happened, a test OU nobody deleted, a stub left over from a migration years ago. Before restructuring, cleaning up, or handing an OU inventory to an auditor, you want a full list with enough context to know what's actually in use.
The quick answer
Get-ADOrganizationalUnit -Filter * | Select-Object Name, DistinguishedName
A more useful reporting script
#requires -Modules ActiveDirectory
<#
.SYNOPSIS
Reports every Organizational Unit in Active Directory, with child object
counts and GPO links.
#>
[CmdletBinding()]
param(
[string]$SearchBase,
[switch]$EmptyOnly,
[string]$OutputCsv
)
Import-Module ActiveDirectory -ErrorAction Stop
$ouParams = @{
Filter = '*'
Properties = @('DistinguishedName', 'ProtectedFromAccidentalDeletion', 'whenCreated', 'gPLink')
}
if ($SearchBase) { $ouParams['SearchBase'] = $SearchBase }
$ous = Get-ADOrganizationalUnit @ouParams
$results = foreach ($ou in $ous) {
$childCount = @(Get-ADObject -Filter * -SearchBase $ou.DistinguishedName -SearchScope OneLevel).Count
$gpoLinkCount = if ($ou.gPLink) { ([regex]::Matches($ou.gPLink, '\[LDAP://')).Count } else { 0 }
if ($EmptyOnly -and $childCount -ne 0) { continue }
[PSCustomObject]@{
Name = $ou.Name
DistinguishedName = $ou.DistinguishedName
ChildObjectCount = $childCount
LinkedGpoCount = $gpoLinkCount
ProtectedFromAccidentalDeletion = $ou.ProtectedFromAccidentalDeletion
WhenCreated = $ou.whenCreated
}
}
if (-not $results) {
Write-Host "No OUs found matching the given criteria." -ForegroundColor Yellow
return
}
$sorted = $results | Sort-Object DistinguishedName
Write-Host "Found $(@($sorted).Count) OU(s)." -ForegroundColor Cyan
$sorted | Format-Table Name, ChildObjectCount, LinkedGpoCount, ProtectedFromAccidentalDeletion, WhenCreated -AutoSize
$unprotectedCount = @($sorted | Where-Object { -not $_.ProtectedFromAccidentalDeletion }).Count
if ($unprotectedCount -gt 0) {
Write-Host "$unprotectedCount OU(s) are not protected from accidental deletion." -ForegroundColor Red
}
if ($OutputCsv) {
$sorted | Export-Csv -Path $OutputCsv -NoTypeInformation
Write-Host "Exported results to $OutputCsv" -ForegroundColor Green
}
-EmptyOnly filters straight to OUs with zero direct child objects — the strongest signal an OU is a cleanup candidate rather than something still in active use.
Finding empty OUs manually
If you just need the empty-OU check without the rest of the report:
Get-ADOrganizationalUnit -Filter * | Where-Object {
-not (Get-ADObject -Filter * -SearchBase $_.DistinguishedName -SearchScope OneLevel)
}
Why unprotected OUs matter
Every OU created through ADUC's wizard gets Protect object from accidental deletion checked by default, but OUs created via script (New-ADOrganizationalUnit without -ProtectedFromAccidentalDeletion) don't automatically get this — and a Remove-ADOrganizationalUnit -Recursive on an unprotected OU takes every object inside it with it, no confirmation prompt beyond the one for the OU itself. The script flags how many OUs in scope lack this protection so it's not something you find out the hard way.
Where the manual approach runs out of road
A one-off script is fine for a single check. It starts to hurt once you actually need to run this regularly:
- No scheduling. Cron/Task Scheduler can run the script, but now you own the scheduling, the credentials it runs as, and what happens when it silently fails.
- No history. A CSV export is a snapshot. Was this the same five accounts as last month, or a growing list? A single export can't tell you.
- No distribution. Getting the report to the right people (security, IT ops, compliance) on a schedule means building that plumbing yourself.
- Multi-domain/multi-forest pain. Run it once per domain, reconcile the results yourself, and hope naming/OU conventions are consistent across all of them.
- No alerting. If something changes unexpectedly between runs — an account re-enabled, a privileged group gaining a member — nothing tells you until you happen to run the script again.
None of that is a PowerShell problem. It's what turns a script into a product.
What SysFlint AD does instead
Our SysFlint AD runs the same kind of discovery shown above automatically, on a schedule, entirely inside your own network. No agents on domain controllers, no data leaving your environment. It's free, forever.
- Stale and inactive account detection — Enabled accounts that nobody has logged into for 30, 60, or 90+ days — the ones that are still a live attack surface.
- Scheduled reports by email — Daily, weekly, or monthly runs delivered to the right inbox automatically. No Task Scheduler job for you to babysit.
- Multi-domain and forest coverage — Enumerate every domain in the forest in one pass and get one consolidated report instead of one per domain.
If you're currently doing this with a script on a scheduled task, here's the honest comparison between the two, or go straight to the download page.
Get this script, and the rest of them
The script above is part of awesome-it-scripts, SysFlint's free, open-source library of PowerShell scripts for Active Directory and other IT-admin tasks — no signup, no catch, MIT licensed. Grab this one directly from active-directory/Get-ADOrganizationalUnitsReport.ps1, or browse the whole thing.
Find every disabled/locked-out/stale/privileged-account script we've published (plus the FAQ that goes with each one) in the repo's active-directory folder. Star it if it's useful — new scripts land there regularly.