Published on

How to Find and Restore Recently Deleted Users in Active Directory (PowerShell Script)

Someone deletes the wrong account — a fat-fingered cleanup script, a bulk offboarding job that caught the wrong OU, an admin who deleted instead of disabled. If the AD Recycle Bin is enabled, that account is still fully recoverable for a while. If it isn't, you're dealing with a tombstoned object stripped of most of its attributes, which is a much worse position to be in.

Checking whether the Recycle Bin is even on

Do this before you need it, not after:

Get-ADOptionalFeature -Filter 'Name -eq "Recycle Bin Feature"' | Select-Object EnabledScopes

If EnabledScopes is empty, enable it — this is a one-way operation, once turned on for a forest it can't be turned back off:

Enable-ADOptionalFeature -Identity 'Recycle Bin Feature' -Scope ForestOrConfigurationSet -Target <forest-fqdn>

Finding deleted users

#requires -Modules ActiveDirectory
<#
.SYNOPSIS
    Lists deleted Active Directory user accounts still in the AD Recycle Bin.

.DESCRIPTION
    Queries AD's Deleted Objects container for user accounts (isDeleted -eq
    $true), optionally filtered to deletions within a recent window, and
    optionally exporting the results to CSV. Requires the AD Recycle Bin
    feature to be enabled on the domain (Enable-ADOptionalFeature
    'Recycle Bin Feature') — without it, deleted objects are tombstoned and
    stripped of most attributes rather than fully recoverable. Uses the
    current logged-on user's credentials — run it from a domain-joined
    machine with RSAT installed.

.PARAMETER Days
    Only show users deleted within this many days. If omitted, shows all
    deleted users still in the Recycle Bin (up to the tombstone lifetime).

.PARAMETER OutputCsv
    Path to a CSV file to export the results to. If omitted, results are only
    written to the console.

.EXAMPLE
    .\Find-DeletedADUsers.ps1

    Lists all deleted user accounts currently in the AD Recycle Bin.

.EXAMPLE
    .\Find-DeletedADUsers.ps1 -Days 7 -OutputCsv .\deleted-users.csv

    Lists users deleted in the last 7 days and exports the results to CSV.
#>

[CmdletBinding()]
param(
    [int]$Days,

    [string]$OutputCsv
)

Import-Module ActiveDirectory -ErrorAction Stop

$deletedUsers = Get-ADObject -Filter { isDeleted -eq $true -and ObjectClass -eq 'user' } -IncludeDeletedObjects `
    -Properties DisplayName, sAMAccountName, LastKnownParent, whenChanged |
    Select-Object @{Name = 'Name'; Expression = { if ($_.DisplayName) { $_.DisplayName } else { $_.Name -replace '\nDEL:.*$' } } },
        sAMAccountName,
        @{Name = 'DeletedOn'; Expression = { $_.whenChanged } },
        LastKnownParent

if ($Days) {
    $cutoff = (Get-Date).AddDays(-$Days)
    $deletedUsers = $deletedUsers | Where-Object { $_.DeletedOn -ge $cutoff }
}

$deletedUsers = $deletedUsers | Sort-Object DeletedOn -Descending

if (-not $deletedUsers) {
    Write-Host "No deleted user accounts found in the Recycle Bin." -ForegroundColor Yellow
    return
}

Write-Host "Found $(@($deletedUsers).Count) deleted user account(s) in the Recycle Bin." -ForegroundColor Cyan
$deletedUsers | Format-Table Name, sAMAccountName, DeletedOn, LastKnownParent -AutoSize

if ($OutputCsv) {
    $deletedUsers | Export-Csv -Path $OutputCsv -NoTypeInformation
    Write-Host "Exported results to $OutputCsv" -ForegroundColor Green
}

NOTE

Deleted objects get \nDEL:<GUID> appended to their name when they're removed — that's why the script falls back to stripping that suffix off Name when DisplayName wasn't set on the original object (not every account created via PowerShell has one).

Restoring what you find

Once you've confirmed the right account, Restore-ADObject brings it back — group memberships restore automatically as part of this if the Recycle Bin was enabled at the time of deletion:

Get-ADObject -Filter {isDeleted -eq $true -and SamAccountName -eq 'jdoe'} -IncludeDeletedObjects |
    Restore-ADObject

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.

  • 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/Find-DeletedADUsers.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.