Hi Ashley Cave
Thank you for reaching out to Microsoft Q&A forum
Based on my research, the reason of this behavior because the new Approvals feature in SharePoint is fundamentally different from classic content approval. Classic approval relies on the ModerationStatus column and can be automated via Power Automate or PnP.
In contrast, modern Approvals is powered by the Teams Approvals service and manages its own metadata fields, which are system-controlled. This design prevents direct updates through PowerShell or Flow actions. As far as I know, Microsoft currently does not provide a native bulk approval option for modern Approvals.
You can review the full details in the official documentation here: Approvals in Lists & Document Libraries.
However, from my perspective view, you can try below workaround methods to see if it can help you in this situation
Kindly temporarily turn Require content approval ON for the target library, bulk mark existing files as Approved using the classic ModerationStatus field, then (optionally) turn it OFF again after migration, link instruction: Require approval of items in a list or library
You can try to use this cmdlet:
<#
Notes:
- Runs once for historical items. Do NOT use for the new Teams/Approvals feature.
- Tested with PnP.PowerShell 2.x on PowerShell 7.x.
#>
param(
[Parameter(Mandatory=$true)]
[string]$SiteUrl, # e.g. https://contoso.sharepoint.com/sites/Records
[Parameter(Mandatory=$true)]
[string]$LibraryName, # e.g. "Documents" or "Contracts"
[switch]$DisableModerationAfter, # add -DisableModerationAfter to turn OFF when done
[int]$PageSize = 2000,
[int]$MaxDegreeOfParallelism = 4
)
Write-Host "Connecting to $SiteUrl ..." -ForegroundColor Cyan
Connect-PnPOnline -Url $SiteUrl -Interactive
# 1) Ensure moderation (classic content approval) is ON
Write-Host "Enabling classic content approval on '$LibraryName' ..." -ForegroundColor Cyan
Set-PnPList -Identity $LibraryName -EnableModeration $true
# 2) Pull items in pages; skip folders; skip already approved
# ModerationStatus values: Approved=0, Rejected=1, Pending=2, Draft=3, Scheduled=4
Write-Host "Querying items in '$LibraryName' ..." -ForegroundColor Cyan
$items = Get-PnPListItem -List $LibraryName -PageSize $PageSize -Fields "ID","FileLeafRef","FSObjType","OData__ModerationStatus"
# Filter to files (FSObjType = 0) and not Approved (<> 0)
$targets = $items | Where-Object { $_["FSObjType"] -eq 0 -and ($_.FieldValues["OData__ModerationStatus"] -ne 0) }
Write-Host ("Items to approve: {0}" -f $targets.Count) -ForegroundColor Yellow
if ($targets.Count -eq 0) {
Write-Host "No items require approval. Exiting." -ForegroundColor Green
return
}
# 3) Approve in parallel batches with simple retry
$throttle = [System.Threading.SemaphoreSlim]::new($MaxDegreeOfParallelism, $MaxDegreeOfParallelism)
$errors = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
$scriptBlock = {
param($item, $LibraryName)
$attempts = 0
do {
try {
# Set classic ModerationStatus to Approved (0)
Set-PnPListItem -List $LibraryName -Identity $item.Id -Values @{ "OData__ModerationStatus" = 0 } -ErrorAction Stop
Write-Host ("Approved: ID={0}, Name={1}" -f $item.Id, $item.FieldValues["FileLeafRef"]) -ForegroundColor Green
return
}
catch {
$attempts++
if ($attempts -lt 5) {
$delay = :Min(30, 5 * $attempts) # backoff up to 30s
Write-Host ("Retry {0} for ID={1}: {2}. Waiting {3}s..." -f $attempts, $item.Id, $_.Exception.Message, $delay) -ForegroundColor DarkYellow
Start-Sleep -Seconds $delay
}
else {
Write-Host ("FAILED after retries: ID={0}: {1}" -f $item.Id, $_.Exception.Message) -ForegroundColor Red
$global:errors.Add([PSCustomObject]@{
Id = $item.Id
Name = $item.FieldValues["FileLeafRef"]
Error= $_.Exception.Message
})
return
}
}
} while ($true)
}
Write-Host "Approving items in parallel (max $MaxDegreeOfParallelism) ..." -ForegroundColor Cyan
$jobs = @()
foreach ($t in $targets) {
$throttle.Wait()
$jobs += Start-Job -ScriptBlock {
param($t, $LibraryName)
Import-Module PnP.PowerShell
# Reconnect inside job using the current context token (PnP cmdlets handle this via Connection persistence in PS7).
# If your environment requires explicit reconnection, use Connect-PnPOnline with -Credentials or -Interactive here.
& $using:scriptBlock $t $LibraryName
} -ArgumentList $t, $LibraryName
# Release semaphore when job completes
Register-ObjectEvent -InputObject $jobs[-1] -EventName StateChanged -SourceIdentifier ("job_" + $jobs[-1].Id) -Action {
$throttle.Release() | Out-Null
} | Out-Null
}
# Wait for jobs
$jobs | ForEach-Object { Receive-Job -Job $_ -Wait -AutoRemoveJob | Out-Null }
# 4) Optional: turn OFF moderation again (return to normal library behavior)
if ($DisableModerationAfter.IsPresent) {
Write-Host "Disabling classic content approval on '$LibraryName' ..." -ForegroundColor Cyan
Set-PnPList -Identity $LibraryName -EnableModeration $false
}
# 5) Summaries
if ($errors.Count -gt 0) {
Write-Host "Some items failed to approve. Writing errors to BulkApproveErrors.csv ..." -ForegroundColor Yellow
$errors | Export-Csv -Path "BulkApproveErrors.csv" -NoTypeInformation -Encoding UTF8
} else {
Write-Host "All targeted items set to Approved." -ForegroundColor Green
}
Write-Host "Done."
For more information: https://learn.microsoft.com/en-us/sharepoint/dev/business-apps/power-automate/guidance/require-doc-approval
Hope my answer will help you, for any further concern, kindly let me know in the comment section.
If the answer is helpful, please click "Accept Answer" and kindly upvote it. If you have extra questions about this answer, please click "Comment".
Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.