Short answer: run .\Get-ADInactiveAccounts.ps1 -Days 90 -ExportCsv C:\Reports\inactive.csv to list enabled users and computers whose lastLogonTimestamp is older than 90 days. Add -IncludeNeverLoggedOn to catch accounts that were created more than 90 days ago and never used. When you have reviewed the list, -Disable -WhatIf shows what would be disabled and -Disable does it, always skipping critical system objects and anything you -Exclude.
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
Stale accounts are an easy win for both security and licensing. This script is the ready-to-run version of the method in our guide Find inactive AD users and computers: the same lastLogonTimestamp logic, with parameters, a CSV and a guarded disable step.
- Server-side filter. The domain controller evaluates
(lastLogonTimestamp<=<cutoff>), so only stale accounts come back, even in large domains. - Never logged on, but not brand new.
-IncludeNeverLoggedOnadds accounts with nolastLogonTimestampwhosewhenCreatedis also older than the cutoff, so last week’s new starters are not flagged. - Users, computers or both, enabled only by default (
-IncludeDisabledto see the disabled ones too). - Useful columns: last logon date, days inactive, created date, password last set, operating system for computers, description.
- Safe disable.
-Disablesupports-WhatIfand-Confirm(it asks for each account by default), never touches objects markedisCriticalSystemObject(for example the built-in Administrator and domain controller computer accounts), honours-Excludepatterns, and records the result per account in the Action column.
Requirements
- Windows PowerShell 5.1 or PowerShell 7 on Windows, with the ActiveDirectory module (RSAT).
- Read access to AD for the report (the default for domain users).
- For
-Disable: the right to disable the accounts in scope, for example delegated on the OUs. - Domain functional level Windows Server 2003 or later, which is when
lastLogonTimestampis maintained (any current domain).
Download and first run
- Copy the script from the box on this page (or use the download button) and save it as
C:\Scripts\Get-ADInactiveAccounts.ps1. - If you downloaded the file, remove the “downloaded from the internet” mark so the execution policy lets it run:
Unblock-File C:\Scripts\Get-ADInactiveAccounts.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 .\Get-ADInactiveAccounts.ps1 -Full
.\Get-ADInactiveAccounts.ps1 -Days 90
.\Get-ADInactiveAccounts.ps1 -Days 90 -IncludeNeverLoggedOn -ExportCsv C:\Reports\inactive.csv
Options
| Parameter | What it does | Default |
|---|---|---|
-Days | Inactivity threshold in days (15 to 3650) | 90 |
-ObjectType | Users, Computers or Both | Both |
-IncludeNeverLoggedOn | Add never-used accounts older than -Days | Off |
-IncludeDisabled | Also list accounts that are already disabled | Off |
-SearchBase | Limit to one OU (DN) | Whole domain |
-Exclude | sAMAccountName patterns to leave out, e.g. "svc-*","SQL01$" | None |
-Server | Domain or DC to query | Current domain |
-Disable | Disable the reported enabled accounts (with -WhatIf / -Confirm) | Off |
-ExportCsv | Write a CSV file | Not written |
-PassThru | Send the row objects down the pipeline | Off |
The minimum of 15 days is deliberate: lastLogonTimestamp can lag the real last logon by up to 14 days, so a shorter threshold would flag active people.
Usage examples
# Users and computers with no logon for 90 days, to CSV
.\Get-ADInactiveAccounts.ps1 -Days 90 -ExportCsv C:\Reports\inactive.csv
# Workstations idle for 120 days, including ones that never joined properly
.\Get-ADInactiveAccounts.ps1 -ObjectType Computers -Days 120 -IncludeNeverLoggedOn -SearchBase "OU=Workstations,DC=contoso,DC=com"
# Preview a disable run, leaving service accounts alone
.\Get-ADInactiveAccounts.ps1 -ObjectType Users -Days 120 -Exclude "svc-*" -Disable -WhatIf
# Disable without per-account prompts, keeping a record
.\Get-ADInactiveAccounts.ps1 -ObjectType Users -Days 120 -Exclude "svc-*" -Disable -Confirm:$false -ExportCsv C:\Reports\disabled-2026-10.csv
# Stale servers (operating system contains "Server")
.\Get-ADInactiveAccounts.ps1 -ObjectType Computers -Days 60 -PassThru | Where-Object OperatingSystem -like '*Server*'
Before you disable anything
Disabling accounts can stop services. Service accounts, shared mailboxes’ users, break-glass admin accounts and computers that are only switched on occasionally (lab machines, spare laptops, DR servers) can all look inactive. Review the CSV with the account owners, add exceptions with -Exclude, and always run -Disable -WhatIf first.
- Export the list without
-Disableand send it to the owners or managers for review. - Make sure the AD Recycle Bin is on (see enable the AD Recycle Bin) so later deletions can be undone; disabling itself is reversible with
Enable-ADAccount. - Run with
-Disable -WhatIfand check the “What if” lines. - Run with
-Disable -ExportCsv; the Action column records Disabled, Skipped or the error for every account. - Leave disabled accounts for a grace period (30 to 90 days is common) before you move or delete them.
CSV columns
The example output further down is from our lab run. The columns are:
| Column | Meaning |
|---|---|
| Name, SamAccountName, ObjectType | The account; ObjectType is User or Computer |
| Enabled | From userAccountControl |
| LastLogonDate | From lastLogonTimestamp; empty if never logged on |
| DaysInactive | Days since LastLogonDate, or since creation if never logged on |
| NeverLoggedOn | True if there is no lastLogonTimestamp |
| Created, PasswordLastSet | From whenCreated and pwdLastSet |
| OperatingSystem, Description | Helpful when deciding about computers and service accounts |
| Critical | True for objects marked isCriticalSystemObject (never disabled) |
| DistinguishedName | Full DN |
| Action | Empty, Disabled, Already disabled, Skipped: critical system object, WhatIf / skipped, or the error |
Example output
With -Days 30 -IncludeNeverLoggedOn on the new lab domain nothing qualified yet, because every account was created the same day. The script compares against the creation date for never-used accounts, so a brand-new account is not reported as stale:
0 account(s) written to C:\srvs-lab\out\inactive.csv
Inactive for 30+ days (before 2026-09-06): 0 user(s), 0 computer(s).
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\Get-ADInactiveAccounts.ps1 -Days 90 -IncludeNeverLoggedOn -ExportCsv C:\Reports\inactive.csv'
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 7am
$principal = New-ScheduledTaskPrincipal -UserId 'CONTOSO\gmsa-adreport$' -LogonType Password
Register-ScheduledTask -TaskName 'AD inactive accounts report' -Action $action -Trigger $trigger -Principal $principal
Schedule the report, not the disable step. Automatic disabling without a human review is how service accounts and break-glass accounts get switched off by surprise.
How it works
- Works out the cutoff date and converts it twice: to a Windows file time for
lastLogonTimestampand to LDAP generalized time (yyyyMMddHHmmss.0Z) forwhenCreated. - Builds one LDAP filter per object type, for example
(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(lastLogonTimestamp<=...)), and adds the never-logged-on branch when asked. - Runs
Get-ADObject -LDAPFilterwith a page size of 500 and reads only the attributes it needs. - Applies
-Exclude, calculates days inactive and builds the rows. - With
-Disable, callsDisable-ADAccountfor each eligible row afterShouldProcessapproval.
Limitations
- lastLogonTimestamp lags. By default it is 9 to 14 days behind the real last logon. That is why the threshold starts at 15 days.
- Not every sign-in updates it. Activity that does not authenticate against AD (for example cloud-only sign-ins to Microsoft 365 for a synced user) does not move
lastLogonTimestamp. Check Entra ID sign-in activity before disabling hybrid users. - Computers that are off for long periods (spares, DR) look inactive; exclude them or put them in an OU you do not scan.
- One domain per run; use
-Serverfor others. - Not yet run against a live domain (see the note at the top).
Official documentation: The lastLogonTimestamp attribute (Microsoft AskDS) · Search-ADAccount -AccountInactive · UserAccountControl flags
Related: Find Inactive AD Users and Computers: PowerShell Cleanup in 5 Steps · Get-ADUser PowerShell Examples: 25 Queries for Active Directory · Enable and use the Active Directory Recycle Bin to restore deleted objects · Employee Offboarding Checklist: Secure AD, M365 and Workspace Steps · Install RSAT on Windows 11: 5 Methods, Including Offline
See also: Export AD Users to CSV: PowerShell Script with Last Logon · 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 · AD Health Check Report: dcdiag and repadmin PowerShell Script
The script
# Inactive AD Accounts Report: PowerShell Script with Safe Disable (v1.0.0) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/inactive-ad-accounts-report/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
<#
.SYNOPSIS
Find AD user and computer accounts with no logon for N days (lastLogonTimestamp), report them to CSV and
optionally disable them.
.DESCRIPTION
Read-only unless -Disable is used.
Finds enabled accounts whose lastLogonTimestamp is older than -Days, using an LDAP filter that the DC
evaluates (no need to pull every account). With -IncludeNeverLoggedOn it also returns accounts that have
never logged on (lastLogonTimestamp empty) and were created more than -Days ago, so brand-new accounts
are not reported.
lastLogonTimestamp is replicated to every DC but, by design, can be 9-14 days behind the real last
logon (default ms-DS-Logon-Time-Sync-Interval of 14 days minus a random 0-5 days). Keep -Days well above
14; 90 is the common choice.
-Disable disables the reported accounts with Disable-ADAccount. It supports -WhatIf and -Confirm
(ConfirmImpact High, so you are asked per account unless you pass -Confirm:$false). Objects marked
isCriticalSystemObject (built-in Administrator, domain controller computer accounts, krbtgt ...) and
anything matching -Exclude are never disabled. Export the CSV first: it is your record of what was changed.
.PARAMETER Days
Inactivity threshold in days (default 90, minimum 15).
.PARAMETER ObjectType
Users, Computers or Both (default Both).
.PARAMETER IncludeNeverLoggedOn
Also report accounts that never logged on and are older than -Days.
.PARAMETER IncludeDisabled
Also report accounts that are already disabled (they are never touched by -Disable).
.PARAMETER SearchBase
Limit the search to this OU (DN).
.PARAMETER Exclude
sAMAccountName patterns to leave out of the report and out of -Disable, e.g. "svc-*","SQL01$".
.PARAMETER Server
Domain or DC to query (default: current domain).
.PARAMETER Disable
Disable every reported, enabled account (except critical system objects and -Exclude matches).
.PARAMETER ExportCsv
Write the report to this CSV file (UTF-8).
.PARAMETER PassThru
Output the row objects to the pipeline.
.EXAMPLE
.\Get-ADInactiveAccounts.ps1 -Days 90 -ExportCsv C:\Reports\inactive.csv
.EXAMPLE
.\Get-ADInactiveAccounts.ps1 -ObjectType Computers -Days 120 -IncludeNeverLoggedOn -SearchBase "OU=Workstations,DC=contoso,DC=com"
.EXAMPLE
.\Get-ADInactiveAccounts.ps1 -ObjectType Users -Days 120 -Exclude "svc-*" -Disable -WhatIf
.EXAMPLE
.\Get-ADInactiveAccounts.ps1 -ObjectType Users -Days 120 -Exclude "svc-*" -Disable -Confirm:$false -ExportCsv C:\Reports\disabled-2026-10.csv
.NOTES
Name: Get-ADInactiveAccounts.ps1
Version: 1.0.0
Source: https://srvscripts.com/scripts/inactive-ad-accounts-report/
Guide: https://srvscripts.com/guides/find-inactive-ad-users-computers/
License: MIT
Requires: Windows PowerShell 5.1 or PowerShell 7 on Windows, ActiveDirectory module (RSAT), read access to AD;
for -Disable, the right to disable the accounts in scope.
#>
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param(
[ValidateRange(15, 3650)]
[int]$Days = 90,
[ValidateSet('Users', 'Computers', 'Both')]
[string]$ObjectType = 'Both',
[switch]$IncludeNeverLoggedOn,
[switch]$IncludeDisabled,
[ValidateNotNullOrEmpty()]
[string]$SearchBase,
[ValidateNotNullOrEmpty()]
[string[]]$Exclude,
[ValidateNotNullOrEmpty()]
[string]$Server,
[switch]$Disable,
[ValidateNotNullOrEmpty()]
[string]$ExportCsv,
[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
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-AttributeValue {
param($Entity, [string]$Name)
if ($Entity.PropertyNames -contains $Name) { return $Entity[$Name].Value }
$null
}
$now = Get-Date
$cutoff = $now.AddDays(-$Days)
$cutoffFileTime = $cutoff.ToFileTimeUtc()
$cutoffGenTime = $cutoff.ToUniversalTime().ToString('yyyyMMddHHmmss') + '.0Z'
$classes = switch ($ObjectType) {
'Users' { @{ User = '(objectCategory=person)(objectClass=user)' } }
'Computers' { @{ Computer = '(objectCategory=computer)' } }
default { [ordered]@{ User = '(objectCategory=person)(objectClass=user)'; Computer = '(objectCategory=computer)' } }
}
$enabledClause = if ($IncludeDisabled) { '' } else { '(!(userAccountControl:1.2.840.113556.1.4.803:=2))' }
$activity = "(lastLogonTimestamp<=$cutoffFileTime)"
if ($IncludeNeverLoggedOn) { $activity = "(|$activity(&(!(lastLogonTimestamp=*))(whenCreated<=$cutoffGenTime)))" }
$attrs = 'lastLogonTimestamp', 'whenCreated', 'pwdLastSet', 'userAccountControl', 'description', 'operatingSystem',
'isCriticalSystemObject', 'sAMAccountName', 'name'
$q = @{ Properties = $attrs; ResultPageSize = 500 }
if ($SearchBase) { $q.SearchBase = $SearchBase }
if ($Server) { $q.Server = $Server }
$rows = [System.Collections.Generic.List[object]]::new()
foreach ($kind in $classes.Keys) {
$filter = "(&$($classes[$kind])$enabledClause$activity)"
Write-Verbose "$kind filter: $filter"
try {
$objs = @(Get-ADObject -LDAPFilter $filter @q)
} catch [System.ArgumentException] {
throw "Invalid -SearchBase or filter: $($_.Exception.Message)"
}
foreach ($o in $objs) {
$sam = [string](Get-AttributeValue $o 'sAMAccountName')
$skip = $false
foreach ($pattern in $Exclude) { if ($sam -like $pattern) { $skip = $true; break } }
if ($skip) { Write-Verbose "Excluded: $sam"; continue }
$last = ConvertFrom-FileTimeValue (Get-AttributeValue $o 'lastLogonTimestamp')
$uac = Get-AttributeValue $o 'userAccountControl'
$enabled = $null
if ($null -ne $uac) { $enabled = -not ([int]$uac -band 2) }
$created = Get-AttributeValue $o 'whenCreated'
$inactiveDays = $null
if ($last) { $inactiveDays = [int]($now - $last).TotalDays } elseif ($created) { $inactiveDays = [int]($now - $created).TotalDays }
$rows.Add([pscustomobject]@{
Name = [string](Get-AttributeValue $o 'name')
SamAccountName = $sam
ObjectType = $kind
Enabled = $enabled
LastLogonDate = $last
DaysInactive = $inactiveDays
NeverLoggedOn = (-not $last)
Created = $created
PasswordLastSet = ConvertFrom-FileTimeValue (Get-AttributeValue $o 'pwdLastSet')
OperatingSystem = [string](Get-AttributeValue $o 'operatingSystem')
Description = [string](Get-AttributeValue $o 'description')
Critical = [bool](Get-AttributeValue $o 'isCriticalSystemObject')
DistinguishedName = $o.DistinguishedName
Action = ''
})
}
}
$out = @($rows | Sort-Object ObjectType, @{ Expression = 'DaysInactive'; Descending = $true })
# ---- Optional disable -----------------------------------------------------------------------------------
if ($Disable) {
$dArgs = @{}
if ($Server) { $dArgs.Server = $Server }
foreach ($r in $out) {
if (-not $r.Enabled) { $r.Action = 'Already disabled'; continue }
if ($r.Critical) { $r.Action = 'Skipped: critical system object'; continue }
$what = if ($r.NeverLoggedOn) { 'never logged on' } else { "last logon $($r.LastLogonDate.ToString('yyyy-MM-dd'))" }
if ($PSCmdlet.ShouldProcess("$($r.SamAccountName) ($($r.ObjectType), $what)", 'Disable-ADAccount')) {
try {
Disable-ADAccount -Identity $r.DistinguishedName @dArgs -Confirm:$false
$r.Action = 'Disabled'
} catch {
$r.Action = "Disable failed: $($_.Exception.Message)"
Write-Warning "Could not disable $($r.SamAccountName): $($_.Exception.Message)"
}
} else {
$r.Action = 'WhatIf / skipped'
}
}
}
# ---- Output ---------------------------------------------------------------------------------------------
if ($ExportCsv) {
$out | Export-Csv -LiteralPath $ExportCsv -NoTypeInformation -Encoding UTF8
Write-Information ("{0} account(s) written to {1}" -f $out.Count, $ExportCsv) -InformationAction Continue
}
if ($PassThru) { return $out }
$users = @($out | Where-Object { $_.ObjectType -eq 'User' }).Count
$computers = @($out | Where-Object { $_.ObjectType -eq 'Computer' }).Count
Write-Information ("Inactive for {0}+ days (before {1:yyyy-MM-dd}): {2} user(s), {3} computer(s)." -f $Days, $cutoff, $users, $computers) -InformationAction Continue
if (-not $ExportCsv -and $out.Count) {
$out | Format-Table Name, ObjectType, Enabled, LastLogonDate, DaysInactive, NeverLoggedOn, Action -AutoSize
}
cae68190a027a36f412677bde5176b5e431085c5ab5aa2600596b32219b3ebbacurl -fsSL -o Get-ADInactiveAccounts.ps1 https://scr.srvscripts.com/inactive-ad-accounts-report/Get-ADInactiveAccounts.ps1 && curl -fsSL https://scr.srvscripts.com/inactive-ad-accounts-report/Get-ADInactiveAccounts.ps1.sha256 | sha256sum -cInvoke-WebRequest -Uri 'https://scr.srvscripts.com/inactive-ad-accounts-report/Get-ADInactiveAccounts.ps1' -OutFile 'Get-ADInactiveAccounts.ps1'; if ((Get-FileHash 'Get-ADInactiveAccounts.ps1' -Algorithm SHA256).Hash -eq 'CAE68190A027A36F412677BDE5176B5E431085C5AB5AA2600596B32219B3EBBA') { '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 find inactive users in Active Directory with PowerShell?
Query enabled users whose lastLogonTimestamp is older than your cutoff. This script does it with a server-side LDAP filter: Get-ADInactiveAccounts.ps1 -ObjectType Users -Days 90.
How many days should count as inactive?
90 days is a common choice for users and computers. Do not go below about 15 days, because lastLogonTimestamp can be up to 14 days behind.
Why is an active user in the list?
They may only use services that do not authenticate against AD, such as cloud-only Microsoft 365 sign-ins, or they work on a schedule longer than your threshold. Add them to -Exclude.
Is it safe to disable inactive accounts automatically?
Not without review. Run the report, have owners confirm, then disable with -WhatIf first. Disabling is reversible with Enable-ADAccount.
What is the difference between this and Search-ADAccount -AccountInactive?
Both rely on lastLogonTimestamp. This script adds a clear never-logged-on rule based on the creation date, exclusions, extra columns and a guarded disable step with a CSV record.
Does it delete accounts?
No. It can only disable accounts, and only with -Disable. Deleting is left to you after a grace period.