You added an archive step to a Purview retention label, and now nothing tells you what actually happened in your sites. This guide provides a read-only PowerShell and Microsoft Graph script that inventories archived files site by site. It also shows how to cross-reference the results with the audit log to find out who triggered each archive operation.
Why SharePoint retention remains a black box
Since August 2026, a Purview retention label can include an archive step. For example: a label keeps items for five years and then deletes them, and you insert archiving at three years. At that point, Purview moves the files into Microsoft 365 Archive and keeps them there until retention expires. Storage costs less, and archived content falls outside the scope that Microsoft 365 Copilot can use.
However, retention processing runs as a background task, with no progress log that administrators can use. The only practical check is to read the contents of the sites and confirm that the expected files are archived. On a test of a few files, that is doable by hand. Once an auto-labeling policy is deployed across tens of thousands of items, you need a script.
February 2026
Archive Graph API in preview
Microsoft adds the driveItem archive and unarchive APIs in the /beta version. The documentation states that their use in production is not supported.
August 2026
Archive step in retention labels
Purview retention labels can trigger the archiving of a file before the end of the retention period.
Prerequisites, licenses and permissions
File archiving requires a Microsoft 365 E5 license, and pay-as-you-go billing for Microsoft 365 Archive must be configured beforehand, according to Microsoft Learn. The archive action is not enabled by default: existing policies and labels archive nothing until an administrator modifies them. An empty report may therefore mean that nothing is configured, not that processing is failing.
Before running the script
- Microsoft 365 E5 license and pay-as-you-go billing for Microsoft 365 Archive configured
- Archive action enabled in at least one retention label or policy
- Microsoft Entra ID application with a certificate and the Sites.Read.All and Group.Read.All application permissions, admin consent granted
- Microsoft.Graph.Authentication module installed: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser
- A test site containing at least one archived file (via the label or manually)
| Permission (application) | Use in the script | Note |
|---|---|---|
| Sites.Read.All | Read sites, libraries and list items | Read-only, but across all sites in the tenant: protect the certificate |
| Group.Read.All | List Microsoft 365 groups (Unified type) | Can be replaced by a list of sites that you provide yourself |
| Files.ReadWrite.All | Not used here | Minimum permission for the write archive API, unnecessary for a simple report |
An application with read access across the whole tenant
Sites.Read.All in application mode gives access to the content of all sites. Store the certificate in a secure vault, limit the administrators who can use it, and delete the application if you no longer run the report.
How the script detects an archived file
The Microsoft 365 Archive developer guide states that the enumeration APIs also return archived files, with an empty archiveStatus property for active files. For list items, this field is not returned by default: you must explicitly request $expand=fields($select=_FileArchiveStatus). The script relies on this mechanism. Any file whose _FileArchiveStatus is not empty is counted as archived, and the raw value is kept in the report so that you can see the actual states in your tenant.
One practical detail: accessing the content of an archived file returns a 423 Locked error. A script that tried to download files in order to analyze them would fail. This one reads only metadata (name, size, creation date), which also avoids any side effects on the files.
The traversal follows four levels: Microsoft 365 groups, the root site of each group, the site's document libraries (excluding the Preservation Hold Library), then the list items of each library. The script calls no site cmdlet from the SDK, only Invoke-MgGraphRequest. This should in principle keep it clear of the Graph PowerShell SDK site cmdlet problem that blocks some runs under Azure Automation, but test it before turning it into a runbook.
Implementation: inventorying archived files
The script below authenticates in application mode with a certificate, walks through the sites of Microsoft 365 groups and produces two outputs: a detailed CSV file (one archived file per row) and on-screen summaries by site, by creation year, by extension and by status. It writes nothing to SharePoint. Use -MaxGroups 5 for a first trial.
1#Requires -Modules Microsoft.Graph.Authentication2[CmdletBinding()]3param(4 [Parameter(Mandatory)][string]$TenantId,5 [Parameter(Mandatory)][string]$ClientId,6 [Parameter(Mandatory)][string]$CertificateThumbprint,7 [string]$OutputFolder = (Join-Path ([System.IO.Path]::GetTempPath()) 'ArchiveReport'),8 [int]$MaxGroups = 0 # 0 = tous les groupes ; utile pour un premier test9)10 11$ErrorActionPreference = 'Stop'12$graph = 'https://graph.microsoft.com/v1.0'13 14# Pagination : renvoie chaque élément de toutes les pages via @odata.nextLink15function Get-GraphPaged {16 param([Parameter(Mandatory)][string]$Uri)17 while ($Uri) {18 $page = Invoke-MgGraphRequest -Method GET -Uri $Uri -OutputType PSObject19 $page.value20 $Uri = $page.'@odata.nextLink'21 }22}23 24# Connexion en mode application (certificat)25Connect-MgGraph -TenantId $TenantId -ClientId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome26 27New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null28$csvPath = Join-Path $OutputFolder ('ArchivedFiles-{0:yyyyMMdd-HHmm}.csv' -f (Get-Date))29 30# Le champ _FileArchiveStatus doit être demandé explicitement31$expand = 'driveItem($select=name,size,createdDateTime,file),fields($select=_FileArchiveStatus)'32 33# Groupes Microsoft 365 (type Unified)34$groups = @(Get-GraphPaged -Uri "$graph/groups?`$filter=groupTypes/any(c:c eq 'Unified')&`$select=id,displayName")35if ($MaxGroups -gt 0) { $groups = @($groups | Select-Object -First $MaxGroups) }36Write-Host "Groupes à analyser : $($groups.Count)"37 38$archived = [System.Collections.Generic.List[object]]::new()39$scanned = 040$groupCount = 041 42foreach ($group in $groups) {43 $groupCount++44 Write-Progress -Activity 'Analyse des sites' -Status $group.displayName -PercentComplete (($groupCount / $groups.Count) * 100)45 try {46 # Site racine du groupe47 $site = Invoke-MgGraphRequest -Method GET -Uri "$graph/groups/$($group.id)/sites/root?`$select=id,displayName,webUrl" -OutputType PSObject48 49 # Bibliothèques du site, hors bibliothèque de conservation50 $drives = Get-GraphPaged -Uri "$graph/sites/$($site.id)/drives?`$select=id,name" |51 Where-Object { $_.name -ne 'Preservation Hold Library' }52 53 foreach ($drive in $drives) {54 $uri = "$graph/drives/$($drive.id)/list/items?`$top=500&`$expand=$expand"55 foreach ($item in Get-GraphPaged -Uri $uri) {56 # On ignore les dossiers57 if (-not $item.driveItem.file) { continue }58 $scanned++59 60 # Vide = fichier actif ; toute autre valeur = fichier archivé (ou en cours)61 $status = $item.fields._FileArchiveStatus62 if ([string]::IsNullOrEmpty($status)) { continue }63 64 $name = $item.driveItem.name65 $archived.Add([pscustomobject]@{66 Site = $site.displayName67 SiteUrl = $site.webUrl68 Library = $drive.name69 FileName = $name70 Extension = [System.IO.Path]::GetExtension($name).ToLower()71 SizeBytes = [long]$item.driveItem.size72 CreatedYear = ([datetime]$item.driveItem.createdDateTime).Year73 ArchiveStatus = [string]$status74 })75 }76 }77 }78 catch {79 # Groupe sans site provisionné, accès refusé, etc. : on continue80 Write-Warning "Groupe ignoré ($($group.displayName)) : $($_.Exception.Message)"81 }82}83 84# Export détaillé85$archived | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF886 87# Synthèses88$totalMb = [math]::Round((($archived | Measure-Object -Property SizeBytes -Sum).Sum / 1MB), 2)89Write-Host "`nFichiers analysés : $scanned | fichiers archivés : $($archived.Count) | volume archivé : $totalMb Mo"90 91Write-Host "`n== Par site =="92$archived | Group-Object -Property Site | ForEach-Object {93 [pscustomobject]@{94 Site = $_.Name95 Fichiers = $_.Count96 TailleMo = [math]::Round((($_.Group | Measure-Object -Property SizeBytes -Sum).Sum / 1MB), 2)97 }98} | Sort-Object -Property Fichiers -Descending | Format-Table -AutoSize99 100Write-Host "== Par année de création =="101$archived | Group-Object -Property CreatedYear | Sort-Object -Property Name | Select-Object -Property Name, Count | Format-Table -AutoSize102 103Write-Host "== Par extension =="104$archived | Group-Object -Property Extension | Sort-Object -Property Count -Descending | Select-Object -Property Name, Count | Format-Table -AutoSize105 106Write-Host "== Par état d'archivage =="107$archived | Group-Object -Property ArchiveStatus | Select-Object -Property Name, Count | Format-Table -AutoSize108 109Write-Host "`nRapport CSV : $csvPath"110Disconnect-MgGraph | Out-NullFor a scheduled run under Azure Automation, replace the certificate parameters of Connect-MgGraph with -Identity (managed identity) and remove Write-Progress and the Write-Host calls, which are useless on a server without a console.
Measure before scheduling
Execution time depends on the number of files, because every item in every library is read. On a small tenant, expect on the order of a few minutes; on a large tenant, test first with -MaxGroups to extrapolate.
Running the report and checking the result
Test on a sample
Run the script on a few groups, including a site where you know a file is archived.
1.\Report-ArchivedFiles.ps1 -TenantId '00000000-0000-0000-0000-000000000000' -ClientId '11111111-1111-1111-1111-111111111111' -CertificateThumbprint 'ABCDEF0123456789ABCDEF0123456789ABCDEF01' -MaxGroups 5Check the known file
Open the CSV and verify that the archived test file appears, with the right library and the right creation year. If it is missing, first check that the query does request _FileArchiveStatus (see the troubleshooting table below).
Extend to the whole tenant
Run it again without -MaxGroups. Keep the successive CSV files: by comparing two runs, you measure the real progress of retention processing.
Who archived what: cross-referencing with the audit log
The script reports the status, not the origin. Yet a file can be archived by Purview or manually by a user. Archive operations are recorded in the Microsoft 365 audit log, under the "Archived file" record (FileArchived), which identifies the user or system account behind the operation. This lets you tell a policy-driven archive from a manual one without walking through every site.
In the Microsoft Purview portal, open the Audit solution, run a search over the desired period and filter on the "Archived file" activity. The user column shows the initiator of each operation.
A user account in UserIds indicates a manual archive; a system identity corresponds to retention processing. Then match ObjectId against the CSV rows to confirm that each file archived by policy is the one you expected.
Limits, pitfalls and troubleshooting
Several limits documented by Microsoft Learn explain gaps between what you expect and what the report shows:
- event-based retention is not supported for file-level archiving;
- incompatible items remain active;
- a policy that evaluates an already archived file does not reactivate it;
- after reactivation, a file cannot be archived again, either manually or by policy, for 120 days.
Set-SPOTenant applies to the whole tenant
To hide the Archive action from users while still letting policies archive, the documentation says to run Set-SPOTenant -AllowFileArchive $false after Connect-SPOService. The setting applies to the entire tenant. Manual archiving does not need to be enabled for the policy action to work.
The driveItem archive and unarchive APIs remain in /beta and their use in production is not supported. This report uses none of them, which makes it safe to schedule. If you also automate archiving or reactivation, the PnP PowerShell cmdlet Set-PnPFileArchiveState saves you from writing the calls by hand, but keep its preview status in mind.
| Symptom | Probable cause | Resolution |
|---|---|---|
| The report contains no archived files | The archive action is not enabled in the policy or label, or processing has not happened yet | Edit the policy or label to enable archiving, then check the E5 license and pay-as-you-go billing |
| The _FileArchiveStatus field is missing from responses | The field is not returned by default for list items | Keep $expand=fields($select=_FileArchiveStatus) in the query |
| 403 error on sites | Admin consent missing for Sites.Read.All | Grant consent on the application in Microsoft Entra ID |
| 423 Locked error when accessing content | The file is archived, its content is not readable | Normal behavior: read metadata only, or reactivate the file |
| Warning on a group without a site | Group whose SharePoint site is not provisioned or not accessible | Ignore, or check the group in the Microsoft 365 admin center |
| A labeled file is not archived | Event-based retention, incompatible item, or file reactivated less than 120 days ago | Check the retention type and the reactivation date |
Frequently asked questions
Is a specific license required to archive files with a retention label?
Yes. Microsoft Learn states that file archiving requires a Microsoft 365 E5 license. Pay-as-you-go billing for Microsoft 365 Archive must also be configured before you use it.
Do existing retention policies start archiving automatically?
No. The archive action is not enabled by default: an existing policy or label archives nothing until an administrator modifies it to add this action.
Must manual archiving be allowed for policies to work?
No. You can hide the Archive action from users with Set-SPOTenant -AllowFileArchive $false and still let policies archive. Manual archiving does not need to be enabled for the policy action.
Can a reactivated file be archived again immediately?
No. After reactivation, the file cannot be archived for 120 days, either manually or by policy.
Why does my script get a 423 error on some files?
Accessing the content of an archived file returns a 423 Locked error. The script in this guide reads only metadata and is therefore not affected, unless you extend it to download files.
Where to start
Start with a test site where archiving is enabled, run the script with -MaxGroups, then compare the CSV with the FileArchived audit log. If your results match, move on to the full tenant.
Next actions
- Create the Microsoft Entra ID application with a certificate and consent on Sites.Read.All and Group.Read.All
- Run the script on a sample and compare it with a known archived file
- Run the FileArchived audit search to separate manual archives from Purview archives
- Keep the CSV files to measure progress from one run to the next
- Decide whether the Archive action should remain visible to users
If your tenant is preparing a Copilot deployment, schedule this report as a recurring control: archiving removes old content from the usable scope, and you must be able to prove it. Our technical checklist for preparing the tenant for Copilot and our article on cleaning up SharePoint sprawl before Copilot provide the context, and the guide on optimizing the data lifecycle with Microsoft Purview details label design.
Going further
- Configure Microsoft 365 retention settings: the official reference on the archive action, the license, the limits, the 120-day delay and the FileArchived audit event.
- Developer guidance for Microsoft 365 Archive: how to detect archived files with Graph or SharePoint (archiveStatus, _FileArchiveStatus, 423 error).
- driveItem: archive - Microsoft Graph beta: permissions, response codes and asynchronous mode for archiving folders.
- Set-PnPFileArchiveState (PnP PowerShell): archive or reactivate a file without writing Graph calls.



