Shore Up
A hand moving name-tagged folders from one labelled drawer of a filing cabinet into another drawer, guided by a checklist on a clipboard.
Windows

Automate Moving AD Users Between OUs From a CSV

Ketan Aagja8 min read
No ratings yet

Before you run this

This procedure reads a CSV of user accounts and their destination OUs, then moves each account to its target OU with PowerShell's Move-ADObject. Moving a user changes its distinguished name (DN). That matters because anything scoped by DN or OU — Group Policy links, delegated permissions, and OU-based filters — will start or stop applying to the account the moment it moves. The move itself does not delete the account or its group memberships, and it is reversible if you know where the account came from — which is why the script below records the original OU of every user before it touches anything.

You need an elevated PowerShell session running as a user with rights to move objects in both the source and target OUs (typically a Domain Admin, or an account with delegated "Move" / write permissions on those OUs). You also need the ActiveDirectory module, which ships with RSAT — on a domain controller it is already present; on a management workstation install the "RSAT: Active Directory Domain Services and Lightweight Directory Services Tools" feature.

Test first. Read the script before you run it. Run it against a single test user in a lab or a throwaway OU before you point it at a real CSV. The script defaults to a dry run (-WhatIf) so you can see exactly what would move before anything does. Do not remove that safety until the preview looks right.

Assumptions for this guide: Windows Server 2019/2022 (or Windows 10/11 with RSAT), Windows PowerShell 5.1, a machine joined to the domain, and the ActiveDirectory module available. The syntax is the same under PowerShell 7 with the module imported.

The CSV format

Keep it simple and explicit. Two columns: the account's SamAccountName, and the full DN of the destination OU.

SamAccountName,TargetOU
jsmith,"OU=Sales,OU=Staff,DC=example,DC=com"
adunn,"OU=Sales,OU=Staff,DC=example,DC=com"
rkhan,"OU=Support,OU=Staff,DC=example,DC=com"

Replace example.com and the OU paths with your own. SamAccountName is the pre-Windows-2000 logon name (the short one), which is unique in the domain and unambiguous to look up. Save the file as, say, C:\path\to\moves.csv — substitute your real path everywhere below.

Why not just pipe the CSV into Move-ADObject

Move-ADObject -Identity expects a distinguished name, GUID, or SID — it does not accept a SamAccountName. So the reliable pattern is: look the user up with Get-ADUser to get its object, then move that object. Doing the lookup also lets us fail cleanly on a typo'd account name instead of throwing halfway through a batch.

Step 1 — Record where everyone is now (your rollback file)

Before moving anything, capture each account's current DN. If you need to undo the move, this file tells you where to put each user back.

# Run in an elevated PowerShell session
Import-Module ActiveDirectory

$csv = Import-Csv 'C:\path\to\moves.csv'   # your CSV of moves

$rollback = foreach ($row in $csv) {
    $user = Get-ADUser -Identity $row.SamAccountName -ErrorAction SilentlyContinue
    if ($user) {
        # Original OU = the DN with the leading "CN=..." component stripped off
        $originalOU = ($user.DistinguishedName -split ',', 2)[1]
        [pscustomobject]@{
            SamAccountName = $user.SamAccountName
            OriginalOU     = $originalOU
        }
    } else {
        Write-Warning "Not found, skipping in rollback: $($row.SamAccountName)"
    }
}

$rollback | Export-Csv 'C:\path\to\rollback.csv' -NoTypeInformation

Open rollback.csv and confirm it lists every user with a sensible original OU. Keep it somewhere safe for the duration of the change window.

Step 2 — Preview the moves (dry run)

This loop looks up each user and calls Move-ADObject with -WhatIf, so it prints what it would do without changing anything.

foreach ($row in $csv) {
    $user = Get-ADUser -Identity $row.SamAccountName -ErrorAction SilentlyContinue

    if (-not $user) {
        Write-Warning "User not found: $($row.SamAccountName)"
        continue
    }

    # Verify the target OU actually exists before attempting the move
    if (-not (Get-ADOrganizationalUnit -Identity $row.TargetOU -ErrorAction SilentlyContinue)) {
        Write-Warning "Target OU not found for $($row.SamAccountName): $($row.TargetOU)"
        continue
    }

    Move-ADObject -Identity $user.DistinguishedName -TargetPath $row.TargetOU -WhatIf
}

You should see one What if: line per user, naming the object and the target path. Read them. If a user is being sent somewhere wrong, fix the CSV and run the preview again. Warnings for missing users or missing OUs mean those rows will be skipped — decide whether that is acceptable before continuing.

Step 3 — Run it for real

Once the preview is clean, remove -WhatIf. I keep -Confirm:$false off deliberately here so that the very first destructive step still prompts unless you consciously opt out; if you are moving many accounts and have already vetted the preview, add -Confirm:$false to run unattended.

foreach ($row in $csv) {
    $user = Get-ADUser -Identity $row.SamAccountName -ErrorAction SilentlyContinue

    if (-not $user) {
        Write-Warning "User not found: $($row.SamAccountName)"
        continue
    }

    if (-not (Get-ADOrganizationalUnit -Identity $row.TargetOU -ErrorAction SilentlyContinue)) {
        Write-Warning "Target OU not found for $($row.SamAccountName): $($row.TargetOU)"
        continue
    }

    try {
        Move-ADObject -Identity $user.DistinguishedName -TargetPath $row.TargetOU -ErrorAction Stop
        Write-Host "Moved $($row.SamAccountName) -> $($row.TargetOU)"
    } catch {
        Write-Warning "Failed to move $($row.SamAccountName): $($_.Exception.Message)"
    }
}

The try/catch means one failure (for example, a protected-from-accidental-deletion flag or a permissions gap) is reported and the batch continues rather than dying on the spot.

Note: if a target OU has "Protect object from accidental deletion" set, that protects the OU, not the users inside it, and does not block moving users into it. But if a user object itself is protected, the move can fail — the error text will tell you. You clear that flag per object on the Object tab in Active Directory Users and Computers (enable View → Advanced Features to see it).

Verify it worked

Check the accounts landed where you expect. This re-reads each user and prints its current DN:

foreach ($row in $csv) {
    $user = Get-ADUser -Identity $row.SamAccountName -ErrorAction SilentlyContinue
    if ($user) {
        Write-Host "$($user.SamAccountName): $($user.DistinguishedName)"
    }
}

Each DN should now end with the TargetOU from your CSV. You can also open Active Directory Users and Computers and confirm the accounts appear under the correct OUs. Remember that Group Policy and any OU-delegated rights now apply per the new location — if these users need a policy refresh, gpupdate /force on their machines (or wait for the normal refresh cycle) is the standard way to pull it.

Undo (roll back)

Because you saved rollback.csv in Step 1, reversing the change is the same operation in the other direction. Preview it first:

$rb = Import-Csv 'C:\path\to\rollback.csv'

foreach ($row in $rb) {
    $user = Get-ADUser -Identity $row.SamAccountName -ErrorAction SilentlyContinue
    if ($user) {
        Move-ADObject -Identity $user.DistinguishedName -TargetPath $row.OriginalOU -WhatIf
    }
}

When the What if: output shows each user returning to its original OU, drop -WhatIf and run it again to complete the rollback.

Where to check the exact syntax

For parameter details and behaviour, see Microsoft Learn for Move-ADObject, Get-ADUser, and Get-ADOrganizationalUnit in the ActiveDirectory module reference. If a parameter behaves unexpectedly on your version of the module, treat the Learn page as authoritative over any snippet — including this one.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Bulk-Update AD User Attributes from a CSV

This guide reads a CSV of users and writes two attributes back to Active Directory for each one: Title and Department . Nothing is created or deleted — existing user objects have those two fields overwritten with the values in your file. That overwrite is a real change: if the CSV has a wrong value in a row, that user's title or department is now wrong until you fix it. There is no built-in "undo," so the safety step below is to export the current values first so you can put them back.

9 min read

Clean Up Stale Computer Accounts in Active Directory with PowerShell

This guide finds Active Directory computer accounts that haven't logged in for a long time and retires them in three deliberate stages: report , disable and move to a holding OU , then delete . The purpose is to keep AD tidy and reduce the attack surface of forgotten machine accounts without accidentally killing a computer that's simply been powered off for a while.

8 min read

Batch-Create Active Directory Users from CSV with PowerShell

This guide gives you a PowerShell script that reads a CSV file of new employees and creates one Active Directory user account per row, with a per-row try/catch so that one bad line doesn't halt the whole batch. Its purpose is bulk onboarding — creating dozens or hundreds of accounts without clicking through the console each time.

8 min read

Automate Expiring AD Password Notifications with PowerShell

This script reads every enabled AD user's password-expiry date, works out who expires within a window you set (say, the next 14 days), and emails each of those users a reminder. It reads Active Directory and sends mail — it does not change a single account, reset a password, or alter a policy. That makes it low-risk, but it can still misfire loudly: point it at the whole domain with a bad window and you can email hundreds of people at once, so treat the first live run as the dangerous part.

9 min read