Last month, our HR team dumped 240 scanned policy PDFs on my desk. They needed each one as an editable Word document so they could update the 2026 wording. Opening each file by hand in Word would have eaten a full week.
I wrote a short script instead. It finished in about twenty minutes while I got coffee. That’s the moment most admins realize you can convert PDF to Word using PowerShell and never touch a manual “Save As” again.
In this guide, I’ll walk you through the exact approach I used, the COM automation behind it, how to batch a whole folder, and how to handle the errors you’ll definitely hit along the way.
How PDF to Word Conversion Actually Works in PowerShell
PowerShell has no built-in cmdlet for this. A cmdlet is a small built-in command like Get-ChildItem or Copy-Item. There’s no ConvertTo-WordDocument waiting for you.
What PowerShell can do is control other applications through COM automation. COM (Component Object Model) is an old Windows technology that lets one program drive another. Microsoft Word supports it, and Word 2013 and newer can open PDF files directly.
So the recipe is simple:
- Start a hidden Word instance through COM.
- Open the PDF file in Word.
- Save it as
.docx. - Close everything cleanly.
Word does the heavy lifting. PowerShell just orchestrates it.
You need Microsoft Word installed on the machine running the script. This works in both Windows PowerShell 5.1 and PowerShell 7, though I’ve had slightly better COM stability in 5.1. If PowerShell 7 gives you odd COM errors, run the script with powershell.exe instead of pwsh.exe.
Step 1: Convert a Single PDF to Word
Let’s start with one file. Test small before you loop over hundreds.
# Paths
$pdfPath = "C:\HR\Policies\LeavePolicy.pdf"
$wordPath = "C:\HR\Policies\LeavePolicy.docx"
# Start Word through COM
$word = New-Object -ComObject Word.Application
$word.Visible = $false
$word.DisplayAlerts = 0
# Open the PDF (Word converts it on open)
$doc = $word.Documents.Open($pdfPath, $false, $true)
# 16 = wdFormatDocumentDefault (.docx)
$doc.SaveAs([ref]$wordPath, [ref]16)
# Clean up
$doc.Close($false)
$word.Quit()
Here’s what each part does.
New-Object -ComObject Word.Applicationlaunches Word in the background. You won’t see a window because$word.Visible = $false.$word.DisplayAlerts = 0turns off pop-up dialogs. Without this, Word will show a “Word will now convert your PDF” prompt and your script will hang forever waiting for a click. This one line saves the most headaches.$word.Documents.Open($pdfPath, $false, $true)opens the file. The second argument skips link updates. The third opens it read-only, so you never accidentally corrupt the source PDF.$doc.SaveAs([ref]$wordPath, [ref]16)writes the Word file. The number16is Word’s format code for.docx. Use0if you need the older.docformat.$doc.Close($false)closes the document without saving again.$word.Quit()shuts Word down.
Pro Tip: In my experience, the number one cause of “my script worked once and then stopped” is orphaned WINWORD.EXE processes. Every failed run leaves a hidden Word instance in memory. After five or six, Word starts refusing new COM connections. Always add cleanup, and check with
Get-Process WINWORDwhen things get weird.
Step 2: Release COM Objects Properly
That first script works, but it leaks memory. PowerShell’s garbage collector doesn’t clean up COM objects on its own.
Add this after $word.Quit():
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($doc) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($word) | Out-Null
Remove-Variable doc, word
[GC]::Collect()
[GC]::WaitForPendingFinalizers()
ReleaseComObject tells .NET to drop its reference to Word. [GC]::Collect() forces garbage collection so the process actually exits.
Skip this, and you’ll find twelve WINWORD processes running after a batch job. I learned that the hard way on a file server that ran out of RAM overnight.
Step 3: Convert PDF to Word in Bulk
Now the real value. Let’s process an entire folder of HR policy PDFs.
$sourceFolder = "C:\HR\Policies"
$outputFolder = "C:\HR\Policies\Word"
if (-not (Test-Path $outputFolder)) {
New-Item -Path $outputFolder -ItemType Directory | Out-Null
}
$word = New-Object -ComObject Word.Application
$word.Visible = $false
$word.DisplayAlerts = 0
$pdfFiles = Get-ChildItem -Path $sourceFolder -Filter *.pdf -File
foreach ($pdf in $pdfFiles) {
$target = Join-Path $outputFolder ($pdf.BaseName + ".docx")
if (Test-Path $target) {
Write-Host "Skipping $($pdf.Name) - already converted" -ForegroundColor Yellow
continue
}
try {
$doc = $word.Documents.Open($pdf.FullName, $false, $true)
$doc.SaveAs([ref]$target, [ref]16)
$doc.Close($false)
Write-Host "Converted: $($pdf.Name)" -ForegroundColor Green
}
catch {
Write-Host "Failed: $($pdf.Name) - $($_.Exception.Message)" -ForegroundColor Red
}
}
$word.Quit()
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($word) | Out-Null
[GC]::Collect()
Let’s break down the important lines.
Get-ChildItem -Path $sourceFolder -Filter *.pdf -File grabs only PDF files. The -File parameter (an option you pass to a cmdlet) stops folders from sneaking into the list. If you want to include subfolders, add -Recurse.
$pdf.BaseName gives you the filename without the extension. So LeavePolicy.pdf becomes LeavePolicy, and we bolt .docx onto it.
The Test-Path check plus continue skips files you’ve already converted. That’s the same pattern behind skipping to the next item in a PowerShell ForEach loop — handy when you rerun a job after a crash.
Notice I open Word once outside the loop. Launching Word 240 times would take hours. One instance, many documents.
The Try/Catch block catches conversion failures without killing the whole run. Encrypted or corrupt PDFs will throw, and you want the other 239 files to keep going. If you’re new to this, my guide on Try/Catch in PowerShell covers the pattern in detail.
Step 4: Add Logging So You Know What Failed
Console colors disappear when the script runs as a scheduled task. Write to a log file instead.
$logFile = "C:\HR\Logs\PdfToWord_$(Get-Date -Format 'yyyy-MM-dd').log"
function Write-Log {
param([string]$Message)
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content -Path $logFile -Value "$stamp - $Message"
}
Write-Log "Starting conversion of $($pdfFiles.Count) files"
Then swap your Write-Host calls for Write-Log. Now you can open the log next morning and see exactly which three files choked.
I usually name logs with the date so they don’t overwrite each other. There’s more on this in my post about