File-Level Archiving Is GA and Purview Won’t Tell You What It Moved

Retention labels with an archive action can silently move SharePoint files to restricted storage. The compliance portal has no native report to show you what got archived or why.

A user opens a ticket. A proposal they edited three weeks ago now throws a permissions wall when they try to download it. Nothing was deleted. Nobody changed sharing. The file is exactly where it has always been in the document library — it just isn’t reachable the way it was. Somewhere upstream, a retention label with an archive action did its job perfectly, and the only people who could have told you that are the people who can’t see it either.

That’s file-level archiving for SharePoint Online at general availability. The capability is real and, honestly, useful. The reporting is absent. Here’s the sequence of how we got here and where it bites.

Before GA — the preview, and the quiet confusion about what “archive” means

File-level archiving arrived as a retention-label action. That word matters, because mid-level admins have spent years thinking of retention labels as doing one of two things: keep a file for N years, or delete it after N years. Retention and disposition. Archiving is a third behaviour bolted onto the same lifecycle, and it is not the archiving you already know.

It is not Exchange Online in-place archive — that’s mailboxes and the archive mailbox. It is not inactive mailbox retention. It is not Teams channel archiving, which freezes a channel read-only. And it is adjacent to but distinct from Microsoft 365 Archive, which parks entire sites in cold storage. File-level archiving operates on the individual document. A label carrying an archive action moves that one file to a lower-cost archive storage tier and restricts access to it while it sits there.

During preview, that distinction was easy to wave away because almost nobody had it switched on in anger. The behaviours were theoretical. GA is where theory meets a live tenant.

This week — GA, with three ways to pull the trigger and no way to watch it fire

General availability means production-ready, which in practice means three application paths that don’t announce themselves equally:

  • Auto-apply policy. A retention label with an archive action gets published and auto-applied by a policy — on a keyword, a sensitive info type, a content query. Nobody clicks anything. Files match, the label lands, the archive move queues. This is the silent one.
  • User action in the browser. A user picks the label from the SharePoint document library UI. At least here someone made a decision, even if they had no idea the label carried an archive step.
  • Graph API. A script or an ISV app sets the retention label on a driveItem. Scales beautifully. Audits terribly, unless you wrote the audit yourself.

Where you configure this hasn’t moved: Purview compliance portal → Records management → File plan → create or edit a retention label, and in the label’s retention settings choose the action that archives at the end of — or during — the retention period. The archive action lives inside the label definition, which is exactly why it travels invisibly. Once it’s on the label, every one of those three paths inherits it.

Day one of GA — the reporting gap, stated plainly

Here is the part the GA announcement glides past. The unified audit log will happily record that a retention label was applied to a file. Search for it and you’ll find the record type for label application, the file, the user or policy that applied it, the timestamp. Useful. Also insufficient.

Because “label applied” is not “file archived.” The label application is the trigger. The archive move is an asynchronous outcome that Purview processes on its own schedule, and the portal gives you no view that says show me every file currently sitting in archive tier and why. There is no archiving dashboard. No “archived files” report. No column in the compliance portal that reconciles intent against result. You can see the label went on. You cannot natively see the move completed, and you certainly can’t hand an auditor a list.

The audit log tells you what you asked for. It does not tell you what happened. With archiving, the gap between those two is a support ticket waiting to be filed.

So you build the visibility yourself. Graph is the only place the file-level state actually surfaces.

Hour one of building it — reading the retention label facet off driveItems

Every SharePoint file is a driveItem in Graph, and the retention label applied to it is exposed through the item’s retentionLabel facet. That facet carries the label name, when it was applied, whether it was applied explicitly or inherited, and the retention settings in force. What it does not carry is a tidy boolean called isArchived — so the audit is an inference: enumerate the files, read the labels, and match against the set of labels you already know carry an archive action.

That inference is honest work, not a workaround. It’s the same thing Purview is doing internally; it just refuses to show you.

First, connect with read-only scopes. Nothing here writes anything.

audit.ps1PowerShell
Connect-MgGraph -Scopes "Sites.Read.All","Files.Read.All" -NoWelcome
​
# Reviewed input only. This is a CSV of sites you have deliberately chosen to audit —
# never a live, tenant-wide crawl piped straight into enumeration.
$sites = Import-Csv -Path .\sites-to-audit.csv   # expects a column: SiteId
​
# Labels you KNOW carry an archive action. Confirm these in Purview first.
$archiveLabels = @('Archive - 7yr','Legal Hold - Archive')

Graph throttles, and the enumeration below will hit it on any library of real size. Honour Retry-After; don’t hammer. A wrapper keeps the retry logic out of the main loop.

audit.ps1PowerShell
function Invoke-GraphWithRetry {
    param(
        [Parameter(Mandatory)][scriptblock]$Call,
        [int]$MaxRetries = 5
    )
    for ($attempt = 0; $attempt -lt $MaxRetries; $attempt++) {
        try {
            return & $Call
        }
        catch {
            $status = $null
            try { $status = $_.Exception.Response.StatusCode.value__ } catch {}
​
            if ($status -eq 429 -or $status -eq 503) {
                $retryAfter = 0
                try { $retryAfter = [int]$_.Exception.Response.Headers.RetryAfter.Delta.TotalSeconds } catch {}
                if ($retryAfter -le 0) { $retryAfter = [math]::Pow(2, $attempt) }
                Write-Warning "Throttled (HTTP $status). Backing off $retryAfter s (attempt $($attempt + 1))."
                Start-Sleep -Seconds $retryAfter
                continue
            }
            if ($status -eq 403) {
                Write-Warning "Access denied on a request. Check Sites.Read.All / Files.Read.All consent. Skipping."
                return $null
            }
            throw
        }
    }
    throw "Exceeded retry budget after $MaxRetries attempts."
}

Now walk the document library recursively. Get-MgDriveItemChild -All handles paging through @odata.nextLink for you; the recursion handles folders. For each file, pull the retentionLabel facet and keep only what matches an archive label.

audit.ps1PowerShell
$results = [System.Collections.Generic.List[object]]::new()
​
function Get-ArchivedDriveItems {
    param(
        [Parameter(Mandatory)][string]$DriveId,
        [string]$ItemId = 'root'
    )
​
    $children = Invoke-GraphWithRetry {
        Get-MgDriveItemChild -DriveId $DriveId -DriveItemId $ItemId -All -ErrorAction Stop
    }
    if (-not $children) { return }
​
    foreach ($child in $children) {
        if ($child.Folder) {
            Get-ArchivedDriveItems -DriveId $DriveId -ItemId $child.Id
            continue
        }
​
        $item = Invoke-GraphWithRetry {
            Get-MgDriveItem -DriveId $DriveId -DriveItemId $child.Id `
                -Property 'id,name,webUrl,retentionLabel' -ErrorAction Stop
        }
        $label = $item.RetentionLabel
        if ($label -and $label.Name -in $archiveLabels) {
            $results.Add([pscustomobject]@{
                DriveId          = $DriveId
                FileName         = $item.Name
                WebUrl           = $item.WebUrl
                LabelName        = $label.Name
                AppliedDateTime  = $label.LabelAppliedDateTime
                AppliedExplicitly= $label.IsLabelAppliedExplicitly
                Behaviour        = $label.RetentionSettings.BehaviorDuringRetentionPeriod
            })
        }
    }
}
​
foreach ($site in $sites) {
    $drives = Invoke-GraphWithRetry {
        Get-MgSiteDrive -SiteId $site.SiteId -All -ErrorAction Stop
    }
    foreach ($drive in $drives) {
        Write-Host "Scanning $($site.SiteId) / $($drive.Name)..."
        Get-ArchivedDriveItems -DriveId $drive.Id
    }
}
​
$results | Export-Csv -Path .\archived-files-report.csv -NoTypeInformation -Encoding UTF8
Write-Host "Found $($results.Count) files carrying an archive label."

Read the CSV before you believe it. The facet confirms the label is on the file and when it landed; the actual tier move is Purview’s asynchronous business, and AppliedExplicitly = $false is your flag that an auto-apply policy did this, not a person. That column is the one your help desk will care about.

Six weeks from GA — the ticket you were always going to get

An auto-apply policy matched on a keyword that was broader than whoever wrote it realised. Several hundred working documents picked up the archive label overnight. Users hit access restrictions. The help desk has nothing — no report, no dashboard, no “archived” filter in the portal — so the ticket escalates to you, and you’re reverse-engineering a policy from the symptoms of its success.

If you had the CSV above running on a schedule, this is a diff, not an investigation. You’d see the applied timestamps cluster, the explicit flag come back false across the board, and the label name point straight at the offending policy. That’s the difference between answering in ten minutes and spending an afternoon proving a negative to a compliance lead.

One caution for anyone tempted to script the cleanup. Do not pipe a live query into a Set- or Remove- cmdlet to strip labels in bulk. Export, review, and feed a trimmed, human-checked file back in with -WhatIf first:

audit.ps1PowerShell
# Dry run against a REVIEWED subset, never the raw scan output.
$toReview = Import-Csv -Path .\archived-files-confirmed-for-removal.csv
​
foreach ($row in $toReview) {
    # -WhatIf prints the intended change without making it. Remove only after you trust the list.
    Set-MgDriveItemRetentionLabel -DriveId $row.DriveId -DriveItemId $row.ItemId `
        -BodyParameter @{ name = $null } -WhatIf
}

Next on the calendar

The feature is GA, which means it will spread. Expect the archive action to start appearing inside file plan labels that already exist for retention, quietly inheriting the move onto content that’s been sitting fine for years. Nothing in the portal will warn you when that happens, and nothing in the audit log will distinguish “we kept it” from “we cold-stored it and locked the door.”

So the honest advice is unglamorous. Run the scan now, before you have a reason to, and keep the baseline. The report Microsoft didn’t ship is the one you’ll be judged on when the auditor asks what you archived and why.