Learn how to build a production-grade version governance system in Power Automate that automatically archives document versions, enables self-service restoration, and enforces configurable retention policies across SharePoint sites — with a complete audit trail behind every action.

Here's a scenario that plays out in organizations every week: A regulatory audit arrives, and someone asks your team to produce every version of a critical contract from the past three years — including who changed it, when, and what changed. The documents are in SharePoint. Version history is enabled. But no one ever automated what happens to old versions, which means some files have 200 minor versions bloating storage quotas, others were purged when a site owner got frustrated with the clutter, and the retention policy that Legal asked for in 2021 exists only as a PDF on a forgotten SharePoint page.
If that scenario sounds familiar, this lesson is for you. Document version control is one of those operational concerns that feels optional right up until it isn't — and by then, the cost of getting it wrong is steep. The good news is that Power Automate gives you everything you need to build a production-grade version management system: automated archiving to a preservation library, on-demand version restoration, and enforceable retention policies that run on a schedule without anyone remembering to press a button.
By the end of this lesson, you'll have built three interconnected flows that work together as a version governance system. You'll understand how SharePoint's versioning API works, how to move file versions across site boundaries, and how to build retention logic that's flexible enough for real compliance requirements.
What you'll learn:
You should be comfortable building flows in Power Automate and working with SharePoint as a data source. Specifically, you'll want to have a working grasp of:
If you need to catch up on any of these, the lesson on working with conditions, loops, and variables in Power Automate is a solid refresher before proceeding.
For your SharePoint environment, you'll need:
Before writing a single flow action, you need to understand what you're actually working with. SharePoint's versioning system stores every saved version of a file as a separate snapshot in a hidden versions store. Each version has a version label (like 1.0, 2.0, 2.3), a timestamp, a modified-by user, and — crucially — its own download URL.
When you use the "Get file properties" action in Power Automate, you get metadata about the current version only. To get the full version history, you need to call the SharePoint REST API directly using the Send an HTTP request to SharePoint action.
The endpoint you'll use most is:
GET /_api/web/GetFileByServerRelativeUrl('/sites/YourSite/LibraryName/FileName.docx')/Versions
This returns a JSON array of all versions for that file. Each object in the array looks roughly like this:
{
"VersionLabel": "3.0",
"Created": "2024-11-15T09:23:41Z",
"CreatedBy": {
"LoginName": "i:0#.f|membership|jane.doe@contoso.com",
"Title": "Jane Doe"
},
"CheckInComment": "Final review edits",
"IsCurrentVersion": false,
"Url": "sites/ProjectDocs/Contracts/_vti_history/512/SOW_ClientA.docx"
}
The Url field is the relative path to that specific version's binary. You can construct a download URL by prepending your tenant root — something like https://contoso.sharepoint.com/ — to that value. This is how you'll extract individual versions for archiving.
Key insight: The
IsCurrentVersionfield lets you skip the live version when archiving old copies. When building retention logic, you'll always want to filter this tofalsebefore deleting anything — you never want to archive or delete the current version.
The other important API call is for restoring a version:
POST /_api/web/GetFileByServerRelativeUrl('/sites/YourSite/Library/File.docx')/Versions/RestoreByLabel(versionlabel='2.0')
This promotes a past version to become the current version. The old "current" version doesn't disappear — it gets pushed back into version history. This is safe by default, and it's the basis for the self-service restoration flow you'll build later.
The archiving flow runs on a schedule — daily, weekly, or triggered by a file modification event — and copies qualifying old versions from your source library to a preservation library on another site. "Qualifying" is the key word: you define the rules (age, version label pattern, document type) and the flow applies them consistently.
Trigger: Recurrence — daily at 11 PM
Understanding when to run a scheduled flow versus an event-driven one matters here. If you triggered this on every file modification, you'd be archiving constantly and creating chaos. Nightly runs let changes accumulate, then sweep once. For more on choosing the right trigger type, see the Power Automate triggers: when to start a flow lesson.
Step 1: Get all files modified in the last 24 hours
Add a Get files (properties only) action pointed at your source library. Set the Filter Query to:
Modified ge '@{addHours(utcNow(), -24)}'
Warning: The SharePoint OData filter for date comparison uses the format above, but it can behave unexpectedly if the Modified field is displayed in local time in your list view while actually stored in UTC. Always use
utcNow()in your filter and verify your results in the first few test runs.
Enable pagination if your library has more than 5,000 files. If you're not sure how pagination works with large SharePoint datasets, the lesson on handling pagination and throttling when querying large datasets in Power Automate will save you from a frustrating edge case later.
Step 2: Loop through each file
Add an Apply to each action using the value from step 1. Inside it, you'll build the logic for each file.
Step 3: Get version history via HTTP
Inside the loop, add a Send an HTTP request to SharePoint action:
_api/web/GetFileByServerRelativeUrl('@{items('Apply_to_each')?['{ServerRelativeUrl}']}')/VersionsAccept: application/json;odata=verboseParse the response body with a Parse JSON action. The schema should match the version object structure shown earlier. You can generate it by pasting a sample response into the "Generate from sample" option.
Step 4: Filter to archivable versions
Add a Filter array action on the parsed versions. Your filter condition:
@{item()?['IsCurrentVersion']} is equal to false
AND
@{item()?['Created']} is less than @{addDays(utcNow(), -30)}
This captures versions older than 30 days that aren't the current version. Adjust the day threshold to match your policy. If you're working with legal contracts, you might filter by version label pattern instead — for example, only archiving major versions (where VersionLabel ends in .0) to avoid preserving every minor autosave.
Tip: Build the filter logic incrementally. Start with just
IsCurrentVersion = falseand log the results to your Version Control Log list to see what you'd be archiving before you commit to actually moving files. This "dry run" approach saves a lot of undo work.
Step 5: Archive each qualifying version
Add a nested Apply to each on the filtered array. For each version:
Construct the download URL:
https://contoso.sharepoint.com/@{items('Apply_to_each_versions')?['Url']}
Use an HTTP action (not the SharePoint one — the standard HTTP connector) with method GET and that URL to download the version's binary content. Set authentication to "Active Directory OAuth" with your tenant credentials (or use a service principal — see the integrating Power Automate with Azure Key Vault and Managed Identities guide for doing this properly in production).
Use Create file action pointed at your preservation library. Set the file name to include version context:
@{items('Apply_to_each')?['FileLeafRef']}_v@{items('Apply_to_each_versions')?['VersionLabel']}_@{formatDateTime(items('Apply_to_each_versions')?['Created'], 'yyyyMMdd')}.docx
This produces names like SOW_ClientA_v2.0_20241115.docx — unambiguous and sortable.
Set the file content to body('HTTP_Download_Version').
Step 6: Log to the Version Control Log list
After a successful archive, create a new item in your log list with:
utcNow()This log is your audit trail. It's also how you'll drive the restoration flow in the next section.
Note: Don't try to archive all versions on the first run — you could generate hundreds of HTTP calls per file and hit throttling limits immediately. Consider adding a condition that skips archiving if a log entry already exists for that version. Query your log list at the start of the loop using "Get items" with a filter on FileName and VersionLabel.
Nobody wants to email IT to recover a document version. The restoration flow gives document owners a self-service option: they submit a request via a SharePoint list, and the flow handles the technical work of calling the restore API.
Trigger: When an item is created in a "Version Restore Requests" list
Set up a simple SharePoint list called Version Restore Requests with these columns:
SOW_ClientA.docx)2.0)You could add an approval step here. If your organization requires a second pair of eyes before restoring a version, wire in an approval workflow before the actual restore action. For the base case, let's assume the requester is the document owner and has authority to restore.
Step 1: Validate the request
Before calling the API, confirm the version label exists in version history. Add a Send an HTTP request to SharePoint action identical to the one in the archive flow (GET Versions endpoint), then use a Filter array to check if the requested version label exists:
@{item()?['VersionLabel']} is equal to @{triggerOutputs()?['body/VersionLabel']}
Add a Condition action: if the filter array length is 0, update the request item's Status to "Rejected" and send the requester a notification explaining the version doesn't exist. Terminate the flow.
Step 2: Call the Restore API
If the version exists, add a Send an HTTP request to SharePoint action:
_api/web/GetFileByServerRelativeUrl('/sites/ProjectDocs/@{triggerOutputs()?['body/DocumentLibrary']}/@{triggerOutputs()?['body/FileName']}')/Versions/RestoreByLabel(versionlabel='@{triggerOutputs()?['body/VersionLabel']}')
Accept: application/json;odata=verbose
X-RequestDigest: (use the form digest from a prior HTTP call to /_api/contextinfo)
Warning: The RestoreByLabel POST call requires a valid request digest token — SharePoint's CSRF protection mechanism. Before calling Restore, add another HTTP action that POSTs to
/_api/contextinfowith an empty body. Parse the response to extractFormDigestValueand pass it as theX-RequestDigestheader. Skipping this step causes a 403 error that's easy to misdiagnose.
Step 3: Confirm and notify
After the restore POST, check the response status code. A 200 means success. Update the request item's Status to "Completed" and send the requester an email or Teams notification confirming which version is now live. Log the action in your Version Control Log list just as you did in the archiving flow.
If the status code is anything other than 200, update Status to "Failed," log the error body, and alert the flow owner. The lesson on master error handling and retry patterns in Power Automate goes deep on building this kind of resilient error path.
Retention policy enforcement is where most organizations fall down. The policy exists in a document. Someone reads it occasionally. But the actual enforcement — deleting versions older than 7 years, locking documents after a certain period, moving expired content to a legal hold — is manual, which means it's inconsistent.
You're going to automate it.
Trigger: Recurrence — weekly, Sunday at 2 AM
You want this to run less frequently than the archiver, and at a quiet time when the throttling headroom is largest. For production scheduling considerations, see the lesson on scheduling and managing time-based flows in Power Automate.
Design the policy as data, not code
Instead of hardcoding retention rules in the flow, store them in a SharePoint list called Retention Policies. Columns:
This gives you a policy engine that non-developers can adjust without touching the flow itself. A compliance officer can open that list and say "we need to keep versions for 2,555 days now" and change the number without a flow modification.
Step 1: Load active policies
At the start of the flow, use Get items on your Retention Policies list with IsActive eq 1 as the filter. Apply to each policy in the results.
Step 2: Get all files in the target library
For each policy, use Get files (properties only) against the library at that policy's SiteUrl and LibraryName. You may need to use the Send an HTTP request to SharePoint action here if you're targeting a site different from your flow's default connection — Power Automate's native SharePoint actions let you specify site addresses directly, which is simpler and preferred.
Step 3: Apply MaxVersionCount enforcement
For each file, get its version list. Sort versions by Created date descending (newest first). If the count exceeds MaxVersionCount from the policy, slice the array:
@{skip(body('Parse_JSON_Versions')?['d']?['results'], triggerOutputs()?['body/MaxVersionCount'])}
The skip() function returns everything after the first N items — in other words, the versions you want to delete. For each version in this slice, call the delete endpoint:
DELETE /_api/web/GetFileByServerRelativeUrl('...')/Versions(@{items('Versions_to_delete')?['ID']})
Note you're deleting by version ID, not label. The ID is a numeric integer in the version response object. This is important because DeleteByLabel only works with the RestoreByLabel syntax, and some SharePoint configurations are finicky about it.
Step 4: Apply MaxVersionAgeDays enforcement
Separately (or combined into the same version loop), filter versions where Created is older than addDays(utcNow(), mul(-1, items('Apply_to_each_Policy')?['MaxVersionAgeDays'])). Delete each one using the same DELETE endpoint.
Key insight: Order matters when combining MaxVersionCount and MaxVersionAgeDays. Apply the age filter first, then check the count. If you reverse it, you might delete recent versions to hit a count limit and then find the only remaining versions are old ones that should also be deleted — leaving the file with fewer versions than expected and triggering weird edge cases in subsequent runs.
Step 5: Apply LockAfterDays
For documents where Modified is older than LockAfterDays, you want to set the file to read-only. SharePoint doesn't have a direct "read-only" toggle via REST on individual files in the same way it does for list items, but you can achieve this by:
Use these HTTP calls:
POST /_api/web/GetFileByServerRelativeUrl('...')/ListItemAllFields/breakroleinheritance(copyRoleAssignments=true,clearSubscopes=true)
Then remove the Edit role:
POST /_api/web/GetFileByServerRelativeUrl('...')/ListItemAllFields/roleassignments/getbyprincipalid(@{UserID})/roles/deleteroledefinitions(@{EditRoleId})
Getting the User ID and Role Definition ID requires additional HTTP lookup calls — GET /_api/web/currentuser for the principal and /_api/web/roledefinitions for the role IDs. This is verbose but doable. Alternatively, for a lighter-touch lock, set a custom metadata column like RetentionLocked = Yes and train your governance process to treat that flag as authoritative.
Step 6: Log every policy action
Every deletion, lock, and notification should generate a row in your Version Control Log with Action = "PolicyEnforced", the policy name, and the specific file and version affected. This log is non-negotiable for compliance — if someone asks why a version is gone, your log should have the answer.
Running these flows across multiple sites introduces a permission challenge. The account or service principal running your flow needs appropriate access to every source and destination site. In production, you should be using a dedicated service account or, better, a service principal with an Azure App Registration.
Set the service principal up with the following SharePoint permissions via the /_api/SPAppOnly/GrantAccess endpoint or through the Azure AD portal:
Sites.ReadWrite.All — needed for archiving and restorationSites.FullControl.All — needed for breaking role inheritance in the lock stepWarning:
Sites.FullControl.Allis a broad permission. If your security posture doesn't allow it, scope the lock enforcement to only sites where the service principal has been explicitly granted Site Owner rights. Work with your SharePoint admin to set up fine-grained permissions per site collection if needed.
For managing credentials in flows that touch multiple sites, the guide on securing Power Automate flows in production: managing credentials, connection references, and data loss prevention policies covers exactly this situation and is worth reading in parallel.
If you're deploying these flows across multiple environments — dev, test, prod — package them as a solution and use environment variables for site URLs, library names, and policy list paths. The ALM pipelines and solution-aware flows guide explains this pattern in detail.
A version governance system that runs invisibly is one you'll never trust. You need to know it's running, what it's doing, and when it fails.
Structured logging to SharePoint list
Your Version Control Log list should have enough columns to be useful as a query surface:
| Column | Type | Purpose |
|---|---|---|
| Title | Text | Auto-set to file name |
| Action | Choice | Archived/Restored/Deleted/Locked/Failed |
| PolicyName | Text | Which policy triggered this |
| FileName | Text | The affected file |
| VersionLabel | Text | The affected version |
| SiteUrl | Text | Source site |
| LibraryName | Text | Source library |
| Timestamp | Date/Time | UTC time of action |
| FlowRunId | Text | For cross-referencing with run history |
| ErrorDetail | Multi-line | Error body if action failed |
Setting the FlowRunId is valuable. You can get the current flow's run ID using:
@{workflow()?['run']?['name']}
This lets you go from a log entry directly to the specific flow run in the Power Automate portal for full debugging context.
Weekly summary email
Add a separate scheduled flow that runs Monday morning, queries the Version Control Log for the past week, aggregates counts by action type, and sends a summary email to the governance team. This gives human visibility into what the automation has been doing without requiring anyone to dig into SharePoint lists manually.
Build the complete three-flow version governance system in a test SharePoint environment. Here's a structured progression:
Part 1 — Environment setup (30 minutes)
Part 2 — Build Flow 1 (45 minutes)
Build the archiver. Run it manually first by clicking "Test" with the "I'll perform the trigger action" option. Check that version archive files appear in the VersionArchive library with the correct naming convention. Verify log entries appear in Version Control Log.
Part 3 — Build Flow 2 (30 minutes)
Build the restoration flow. Submit a test restore request via the list. Watch the flow run. Confirm the target file's version history now shows the restored version as current. Verify the log entry.
Part 4 — Build Flow 3 (45 minutes)
Build the retention enforcer. Run it manually. Verify that versions older than your configured threshold are deleted. Check that the log records each deletion with the policy name attached.
Part 5 — Break something intentionally (15 minutes)
Submit a restore request with a version label that doesn't exist (e.g., "99.0"). Confirm the flow handles it gracefully: Status goes to "Rejected," requester gets notified, and no error appears in the flow's run history (the error was handled, not unhandled).
"The version download URL doesn't work — I'm getting a 403"
This almost always means your connection's account doesn't have access to that version's URL. Versions are served from the same domain as your SharePoint site, but the URL structure (_vti_history/...) requires that the account has at least Read access to the library. Double-check that your connection account has access to the source library, not just the destination.
"My filter array returns all versions even when I set IsCurrentVersion to false"
The JSON from SharePoint's REST API uses lowercase field names in odata=verbose format but the actual values may be returned as strings ("true"/"false") rather than booleans, depending on how you're parsing them. Use @{string(item()?['IsCurrentVersion'])} is equal to false (comparing as strings) or be explicit in your Parse JSON schema that this field is a boolean.
"The archiver is re-archiving versions it already archived"
You probably haven't implemented the duplicate check. Before archiving, query your Version Control Log for an item where FileName equals the current file name and VersionLabel equals the current version's label and Action equals "Archived." If that item exists, skip. This is a simple "Get items" action with a filter like:
FileName eq '@{items('Apply_to_each')?['FileLeafRef']}' and VersionLabel eq '@{items('Apply_to_each_versions')?['VersionLabel']}' and Action eq 'Archived'
If the result count is greater than 0, add a "Terminate" inside the loop scope (or skip with a condition) to bypass that version.
"The retention flow times out after 30 days on large libraries"
Power Automate flows have a 30-day maximum run duration, but individual actions time out much sooner. If your library has thousands of files with hundreds of versions each, the nested loops will be slow. Consider breaking the flow into child flows — one parent that iterates over files and fans out to a child flow per file that handles the version logic. The orchestrating child flows and scoped execution in Power Automate lesson covers exactly this architecture.
"The form digest (X-RequestDigest) is expired by the time I use it"
Form digest tokens expire after 30 minutes. If your flow takes a long time to process files before reaching the Restore call, the digest may be stale. Move the contextinfo POST call to immediately before the Restore call — don't retrieve it at the start of the flow and use it 45 minutes later.
"Version deletions are running but the storage quota isn't decreasing"
SharePoint recalculates storage quotas asynchronously. Deleted versions go to the Recycle Bin first and aren't removed from the quota calculation until the Recycle Bin is emptied. If you need immediate quota relief, add a step that deletes the Recycle Bin item as well — but be aware this makes recovery impossible, so only do this after you've confirmed the archive is in place.
Tip: When debugging HTTP calls to SharePoint, add a Compose action after each HTTP response and set its input to
body('Send_an_HTTP_request_to_SharePoint'). This captures the full response body in your run history even when things appear to succeed, making it much easier to spot unexpected response structures. Remove these after debugging — they add overhead.
You've built a three-flow version governance system that automates what most organizations handle manually (or don't handle at all). The archiver captures qualifying versions on a schedule and preserves them with meaningful naming across site boundaries. The restoration flow gives document owners self-service access to past versions without IT involvement. The retention enforcer reads policy configuration from a SharePoint list and enforces age and count limits automatically, with a full audit log behind every action.
The important architectural decisions worth remembering:
From here, consider these extensions:
Version governance isn't glamorous work, but it's exactly the kind of invisible infrastructure that separates organizations that manage information from organizations that hope for the best when the auditors arrive. Build it once, automate it well, and it runs quietly in the background while everyone else is scrambling.