Azure Functions – Inquisitive M365 https://thomasdaly.net Yet another SharePoint / Office 365 blog Wed, 25 Mar 2026 03:39:13 +0000 en-US hourly 1 116451836 Managing Document Library Versioning and Expiration Across a SharePoint Tenant https://thomasdaly.net/2025/06/14/managing-document-library-versioning-tenant/ https://thomasdaly.net/2025/06/14/managing-document-library-versioning-tenant/#respond Sun, 15 Jun 2025 00:00:00 +0000 https://thomasdaly.net/?p=3434 Steampunk furnace with a robotic arm sorting holographic document versions on a conveyor belt, a glowing 60 DAYS display overhead

If you've looked at your SharePoint storage report lately and wondered where all the space went, check version history. Depending on your tenant's configuration — up to 500 major versions, or Microsoft's newer "automatic" model with no fixed cap — there's no expiration by default. Every save, every autosave from co-authoring, every edit creates a version that sticks around forever.

We found libraries where version history was consuming 3-4x the space of the current files. Co-authoring makes it worse — a one-hour session with multiple editors can generate dozens of versions. Multiply that across hundreds of sites and it adds up fast.

Microsoft added tenant-level version controls in the SharePoint Admin Center (Settings > Version history limits), which works for broad policy. But we needed per-library control baked into our site provisioning pipeline — applied consistently whether a site was created today or two years ago, without relying on someone remembering to toggle an admin setting.

We needed a consistent policy: 100 major versions, minor versions enabled, and automatic expiration after 60 days. Applied to every site in the tenant — both existing sites and every new site as it's provisioned. Here's how we did it.


The Versioning Policy

We reviewed storage reports and asked the business how far back they'd ever actually needed to roll back. The answer was almost always days, not months. We landed on:

SettingValueRationale
Major versions100Covers months of active editing, even on busy documents
Minor versionsEnabledSupports draft/publish workflows
Version expiration60 daysAuto-trims anything older than two months

The 60-day expiration is the key lever. It bounds version history regardless of edit frequency. Both major and minor versions are subject to the policy — anything older than 60 days is eligible for cleanup.


Applying Versioning at Provisioning Time

Our sites are provisioned via an Azure Function queue trigger that creates the site, applies a Patterns and Practices (PnP) template, copies template folders, and configures versioning. Here's the relevant section:

# Enable versioning on the Documents library
Write-Host "Enabling versioning on 'Documents' library."
Set-PnPList -Identity "Documents" `
    -EnableVersioning $true `
    -MajorVersions 100 `
    -EnableMinorVersions $true `
    -Connection $newSiteConnection

Write-Host "Setting expiration for versions in 'Documents' library to 60 days."
Set-PnPList -Identity "Documents" `
    -ExpireVersionsAfterDays 60 `
    -Connection $newSiteConnection

Note the two separate Set-PnPList calls. Combining -ExpireVersionsAfterDays with the other versioning parameters in a single call can produce inconsistent results depending on your PnP PowerShell version. We learned that the hard way — split them and it's reliable.


Remediating Existing Sites

New sites are handled, but existing sites still had default settings and months of accumulated version history. We built a bulk remediation script that reads a site inventory CSV and applies the same policy:

Import-Module PnP.PowerShell

# Path to the CSV file containing site inventory
$csvFilePath = "sites.csv"

# Read the CSV file
$sites = Import-Csv -Path $csvFilePath

# Iterate through each active site
foreach ($site in $sites) {
    if ($site.Status -eq 'C' -and -not [string]::IsNullOrWhiteSpace($site.SiteUrl)) {
        try {
            $siteUrl = $site.SiteUrl
            Connect-PnPOnline -Url $siteUrl -ClientId "your-app-registration-client-id"

            # Apply versioning policy
            Set-PnPList -Identity "Documents" `
                -EnableVersioning $true `
                -MajorVersions 100 `
                -EnableMinorVersions $true

            Set-PnPList -Identity "Documents" `
                -ExpireVersionsAfterDays 60

            Write-Host "Successfully updated site: $siteUrl" -ForegroundColor Green
        }
        catch {
            Write-Host "Failed to update site: $siteUrl. Error: $_" -ForegroundColor Red
        }
        finally {
            Disconnect-PnPOnline
        }
    }
}

The CSV tracks every site with a status column — C for active, R for retired. The script skips retired sites and empty URLs, so it's safe to run against the full inventory.


What Happens When Expiration Kicks In

-ExpireVersionsAfterDays 60 doesn't delete old versions immediately — SharePoint handles it as a background job. A few things to know:

  • Background timer job. Versions older than 60 days become eligible for deletion, but actual cleanup can take days on large libraries.
  • Current version is never expired. Only historical versions are affected.
  • Recycle bin delay. Deleted versions go to the recycle bin (93 days total across first and second stage). True storage recovery happens after the recycle bin clears.

If you need to reclaim storage immediately:

Clear-PnPRecycleBinItem -All -Force -Connection $siteConnection

Not reversible. We ran this selectively on sites with the highest storage pressure, not tenant-wide.


Monitoring the Results

After rolling this out, we tracked storage in SharePoint Admin Center. Within a few weeks, total tenant storage dropped as background expiration worked through the backlog. The bigger win was that storage growth flattened — no more unbounded accumulation.


Lessons Learned

Split the Set-PnPList calls. Versioning and expiration in separate calls avoids parameter combination edge cases.

Run remediation quarterly. Settings can drift — someone changes a library manually, or a new library gets created outside the pipeline. Re-running the bulk script catches it.

Watch the recycle bin. Expired versions go to the recycle bin, not into thin air. If you're trying to reclaim storage urgently, clear the recycle bins too.

Communicate before you enforce. We notified users that versions older than 60 days would be automatically cleaned up. Nobody complained — most didn't know versioning existed. But it's good governance to communicate before changing retention behavior.


Wrapping Up

SharePoint versioning is one of those features that works against you when left on autopilot. The defaults are safe but wasteful. By enforcing a consistent policy — 100 major versions with 60-day expiration — across every site in the tenant, we reduced storage consumption significantly and eliminated a growing cost problem.

The approach is two-pronged: apply the policy automatically when new sites are provisioned, and run a bulk remediation script to bring existing sites in line. Both use PnP PowerShell's Set-PnPList, and both can run unattended.

If your tenant storage report has been creeping upward and you're not sure why, check your version history. Chances are that's where the space is going. And if you're managing versioning across a tenant differently — or have hit edge cases with ExpireVersionsAfterDays — I'd like to hear about it in the comments.

]]>
https://thomasdaly.net/2025/06/14/managing-document-library-versioning-tenant/feed/ 0 3434
PnP PowerShell V4 and Azure Functions V3 – First Look https://thomasdaly.net/2020/12/19/pnp-powershell-v4-and-azure-functions-v3-first-look/ https://thomasdaly.net/2020/12/19/pnp-powershell-v4-and-azure-functions-v3-first-look/#respond Sat, 19 Dec 2020 02:06:59 +0000 https://thomasdaly.net/?p=2258 I’m writing this blog to share my experiences with working the latest version of PnP PowerShell V4 this .NET Core version and Azure Functions V3.

Background

Up until a few weeks ago I was able to write PnP PowerShell provisioning code and run it in an Azure Function V1 to do almost anything I wanted in my SharePoint site. I was receiving warnings that Azure was going to be deprecating the V1 functions but not pay much attention. This forced me to try using the new way of using the Azure V3 function which run PowerShell .NET Core and the new PnP PowerShell libraries built on .NET Core.

Azure has removed all ways through the UI to use experiment languages. You can set the PowerShell Azure function to run in V1 mode, however the toggle switch to enable experimental languages has been removed. Rendering this article is no longer valid – https://docs.microsoft.com/en-us/sharepoint/dev/declarative-customization/site-design-pnp-provisioning#create-the-azure-function [UPDATE: 11/25/2020 – Microsoft has updated that article]

The now older version of PnP PowerShell is referred to as PnP PowerShell V3 which is built on the CSOM for .NET 4.5 Libraries. This runs in PowerShell up V5

The newer PnP PowerShell is referred to as PnP PowerShell V4 which is built on the CSOM for .NET Standard 2.0. This run in PowerShell V7.

You can read more about this here – https://github.com/pnp/PnP-PowerShell. A tremendous amount of work has been done to make the libraries available for .NET Core. It’s time we all start using the new V4 version and help the project by submitting defects and contributing.

My Experience

I was faced with many issues which I will outline. While this was my first time experimenting with the new PnP PowerShell and the Azure Functions V3 it was not an easy one. I wrote this article before changes to official Microsoft documentations updates and had to learn by trial and error which can be tedious.

  1. On Demand Loading – the function now use a requirements.ps1 file that contains a manifest of the libraries that your function will using. This is how you load the PnP PowerShell libraries. The files will be downloaded on demand when your function is initialized and will stay in memory until your function goes to sleep.
  2. Problems Connecting / Authentication – I believe there is some issues with Authentication as I spent many hours trying various combinations to work. I would recommend using a certificate as that is the only way I was able to get the functions to work correctly. I think it’s possible some pieces of PnP PowerShell V4 are not working 100% in Azure Functions. I was using 0.2.15-nightly
  3. Different Commands – not a huge deal but you need to be aware that some of the commandlets have changed and should update them accordingly. It’s well documented just be aware.
  4. Pre-Release Version – the PnP PowerShell is still currently pre-release and scheduled to be GA in Jan 2021. We can’t do anything about this, if you need this functionality you will be using the pre-release version. It shouldn’t matter too much if the functionality works with the version of PnP PowerShell that you built it on then you are fine. Somethings may or may not work – always send feedback in the github repo’s for issues.
  5. Timeouts – the on demand loading seemed to timeout my functions [default 5 mins max on Azure Function runtime] this was a problem with the libraries taking 3-4 minutes to load and my code only had 1 minute to finish.

In the host.json file I had increased the timeout to 10 minutes to account for this. At the time of this article I have not tried to upload and host the libraries in my function [more on this in another article]

updating host.json file

Summary

Since writing this article I have made more progress in this area. I will post a follow up in the next week with a walk through of setting up a site script to kick off a Power Automate Flow, that runs the Azure Function to make some modifications to a SharePoint site. I’ve had a lot of fun playing around and experimenting with different flows and can now appreciate this method along with the power behind it. I am excited to see the progress here and looking forward to future updates from the PnP PowerShell team!

]]>
https://thomasdaly.net/2020/12/19/pnp-powershell-v4-and-azure-functions-v3-first-look/feed/ 0 2258