The problem
Two dates have already passed. Publishing new InfoPath forms, or updates to existing ones, was blocked after May 18, 2026. InfoPath Forms Services itself was removed from SharePoint Online after July 14, 2026, the same day InfoPath Client 2013 went out of support, and Microsoft was explicit that there would be no option to extend past it.
So if you’re scoping this now, you’re not planning ahead of a deadline. You’re doing recovery on forms that already stopped rendering.
The useful part: your data didn’t go anywhere. Every InfoPath submission is still an .xml file sitting in its library, downloadable and parseable with a text editor or a few lines of PowerShell. What retired was the rendering path, the piece that turned that XML back into a form in the browser. SharePoint no longer knows how to display it. The file hasn’t moved an inch.
Microsoft’s replacement is the Power Platform, split across two products: Power Apps for the form itself, Power Automate for the workflow that used to run behind it. Most InfoPath forms were half of a pair, so budget for both. A rebuilt form that submits into a list nobody routes for approval has replaced the typing and dropped the process.
The admin UI still shows InfoPath, which fools people
Worth knowing before you go looking for confirmation in the tenant. The SharePoint admin center still lists InfoPath under More features, described as “Enable browser-based InfoPath forms,” with a working Open button.
SharePoint admin center · More features
A month after retirement, the InfoPath entry is still sitting in More features between Records management and Hybrid picker.
Open it and the classic configuration page loads normally, with the browser-enable and render checkboxes both cleared.
SharePoint admin center · InfoPath
The settings page still renders, and the exempt user agent list is still populated. None of it changes anything now that Forms Services is gone.
Don’t read either page as evidence the service is alive. The admin surface outlived the thing it configured, which is normal for retired SharePoint features and misleading if you’re checking whether you still have a problem. The test that matters is whether a user can open a form. They can’t.
Step 1: inventory with a script, not a site crawl
You need PnP PowerShell and an Entra ID app registration with Sites.FullControl.All as an application permission, admin consented. Read-only scopes won’t enumerate content types across every site collection, so you need the write scope even though you’re only reading. Connect with a certificate rather than an interactive login.
# Requires: PnP.PowerShell, app registration with Sites.FullControl.All
Connect-PnPOnline -Url "https://contoso-admin.sharepoint.com" `
-ClientId $clientId -Tenant "contoso.onmicrosoft.com" -CertificatePath $certPath
$sites = Get-PnPTenantSite -IncludeOneDriveSites:$false
$inventory = foreach ($site in $sites) {
try {
Connect-PnPOnline -Url $site.Url -ClientId $clientId `
-Tenant "contoso.onmicrosoft.com" -CertificatePath $certPath
# RootFolder is a nested client object that is NOT loaded by default.
# Ask for it explicitly or the fallback check below throws.
$lists = Get-PnPList -Includes ItemCount, RootFolder
}
catch {
Write-Warning "Could not connect to $($site.Url): $($_.Exception.Message)"
continue
}
foreach ($list in $lists) {
# Per-list try/catch, so one unreadable list doesn't abandon the site
try {
$ct = Get-PnPContentType -List $list -ErrorAction SilentlyContinue |
Select-Object -First 1
$formUrl = $ct.NewFormUrl
$isInfoPath = ($formUrl -like "*.xsn*") -or ($formUrl -like "*FormServer.aspx*")
if (-not $isInfoPath -and $list.RootFolder.ServerRelativeUrl) {
# Some customized list forms never surface the .xsn in NewFormUrl
$templatePath = "$($list.RootFolder.ServerRelativeUrl)/Item/template.xsn"
$isInfoPath = $null -ne (Get-PnPFile -Url $templatePath -ErrorAction SilentlyContinue)
}
[PSCustomObject]@{
SiteUrl = $site.Url
ListTitle = $list.Title
ItemCount = $list.ItemCount
FormTemplate = $formUrl
IsInfoPath = $isInfoPath
}
}
catch {
Write-Warning "Skipped $($list.Title) on $($site.Url): $($_.Exception.Message)"
}
}
}
$inventory | Where-Object IsInfoPath |
Export-Csv "C:\InfoPathInventory.csv" -NoTypeInformation
The fallback check matters more than it looks. A customized list form doesn’t set NewFormUrl to something ending in .xsn. It points at a FormServer.aspx page with the .xsn buried in the query string, so a plain -like "*.xsn" test comes back empty on most tenants. Checking for {list}/Item/template.xsn catches the forms that skip NewFormUrl entirely.
Past about 50 sites, expect this to run for hours, and expect 429 throttling. That’s what the try/catch is for, so one bad site doesn’t kill the job. On a couple hundred sites, pre-filter instead: run a tenant search for FileExtension:xsn and only crawl the sites that come back.
Then run a second pass for form libraries, which are a different list type (BaseTemplate 115, XMLForm) and don’t always surface as a content type form. Sweep ordinary document libraries (BaseTemplate 101) at the same time, because stray .xsn templates get parked there and missed.
foreach ($site in $sites) {
try {
Connect-PnPOnline -Url $site.Url -ClientId $clientId `
-Tenant "contoso.onmicrosoft.com" -CertificatePath $certPath
$formLibraries = Get-PnPList | Where-Object { $_.BaseTemplate -eq 115 }
$docLibraries = Get-PnPList | Where-Object { $_.BaseTemplate -eq 101 }
foreach ($lib in $formLibraries + $docLibraries) {
Get-PnPListItem -List $lib -PageSize 500 |
Where-Object { $_["FileLeafRef"] -like "*.xsn" } |
ForEach-Object {
[PSCustomObject]@{
SiteUrl = $site.Url
Library = $lib.Title
FileName = $_["FileLeafRef"]
}
}
}
}
catch {
Write-Warning "Failed on $($site.Url): $($_.Exception.Message)"
}
}
The output is flat: site, list, item count, template path, and a true/false flag. Export it to CSV and that’s your project plan. ItemCount tells you how much is riding on each row.
Form libraries are the high-risk bucket
List forms and form libraries are not the same problem, and the difference decides your sequencing.
A form library stores every submission as an .xml file that only ever rendered correctly against its .xsn template, and that rendering path is what got removed. If a library holds years of expense claims or incident reports, getting the data out is the first job, not the last.
# Requires: PnP.PowerShell, connected to the site holding the form library
$library = "Expense Claims"
$items = Get-PnPListItem -List $library -PageSize 500
$rows = foreach ($item in $items) {
$content = Get-PnPFile -Url $item["FileRef"] -AsString
[xml]$xml = $content
# InfoPath fields live under the form's own my: namespace. Copy the real
# xmlns:my value from the root element of one submission before running this.
$ns = New-Object System.Xml.XmlNamespaceManager($xml.NameTable)
$ns.AddNamespace("my", "http://schemas.microsoft.com/office/infopath/2003/myXSD")
[PSCustomObject]@{
FileName = $item["FileLeafRef"]
Requestor = $xml.SelectSingleNode("//my:Requestor", $ns).InnerText
Amount = $xml.SelectSingleNode("//my:Amount", $ns).InnerText
SubmittedOn = $xml.SelectSingleNode("//my:SubmittedDate", $ns).InnerText
}
}
$rows | Export-Csv "C:\ExpenseClaimsExtract.csv" -NoTypeInformation
Every form template stamps its own GUID into that namespace URI, so open one submission in a text editor and copy the actual xmlns:my value before pointing this at a different form. Get the namespace wrong and it fails silently, producing blank fields rather than an error, which is the failure mode most likely to survive a casual spot check.
For a repeating table the same child element repeats once per row. Swap SelectSingleNode for SelectNodes, loop it, and write one row per node into the child list described below, using the parent item’s ID as the lookup value.
Output has two sensible destinations. Add-PnPListItem writes straight into a new SharePoint list if the process is still live, or export to CSV for audit-only records nobody will edit again.
Step 2: triage with a rubric
Sort every row from the CSV into one of four buckets. Open each form and check for the specific features. Don’t classify from memory, because the ones that look simple are the ones with a hidden repeating section.
(a) Fields, required validation, show/hide logic. A handful of text fields, some required-field rules, maybe a section that appears based on a dropdown. Rebuild as a customized list form. Budget 2 to 4 hours.
(b) Cascading dropdowns, a lookup to one other list, calculated defaults. Still a list form, but you’re writing Power Fx now instead of clicking through a wizard. Budget about a day.
(c) Repeating tables, data from multiple lists, heavy attachment use. This doesn’t fit inside a single list form. Build a standalone canvas app with a child list behind it. Budget 3 to 5 days.
(d) Digital signatures, print or PDF output, code-behind, admin-approved templates. Stop here. Power Apps has no native signature capture and no print-layout engine, so this isn’t a straight rebuild. Three honest options: a third-party forms product built for signatures and document output, a Power Apps front end paired with a document-generation flow in Power Automate, or retiring the process if nobody can explain why it still needs a signed printed form.
Have the bucket (d) conversation early. It usually turns into a procurement decision rather than a build task, and that takes longer to get approved than anything else on the list.
Translating InfoPath rules to Power Fx
Most InfoPath logic lands on a small set of Power Fx patterns. Keep this open while you rebuild.
| InfoPath rule | Power Fx equivalent |
|---|---|
| Required-field validation | If(IsBlank(DataCardValue1.Text), Notify("Title is required", NotificationType.Error), SubmitForm(Form1)) on the submit button |
| Conditional show/hide | Visible: If(Dropdown1.Selected.Value = "Yes", true, false) on the card |
| Conditional formatting | Fill: If(ThisItem.Status = "Overdue", Color.Red, Color.White) |
| Cascading dropdown filtering | Items: Filter(Cities, Country = Dropdown1.Selected.Value) on the dependent dropdown |
| Default value from user profile | Default: User().Email or User().FullName on the field |
| Submit, then email | Patch(RequestsList, Defaults(RequestsList), {Title: TextInput1.Text}); 'NotifyRequester'.Run(User().Email) |
Step 3: rebuild simple forms as customized list forms
For buckets (a) and (b), skip the standalone app and customize the list form directly.
One wrinkle first, because most write-ups still get this wrong. Integrate is not a tab on the list command bar anymore. It moved under the overflow, so if you go looking for a tab you will not find one. From the list, select the ... button, then Integrate, then Power Apps, then Customize forms.
SharePoint list · overflow menu
All three levels open at once. Integrate sits under the ... button beside Workflows, not on the command bar itself.
That opens the form in Power Apps Studio wired through the SharePointIntegration control. Its OnNew, OnEdit, and OnSave properties fire when a user starts a new item, opens an existing one, or submits, which gives you three clean places to attach validation, defaults, and conditional behaviour without touching the list schema.
Demo · 15 seconds, no audio
Four clicks, recorded in a live tenant in August 2026. The written steps above cover the same ground, so there is nothing in the clip you need sound or sight of to follow along.
Users press New or Edit on the list exactly as before. There’s no separate app link to distribute, and no separate sharing step, because the form inherits the list’s permissions.
Step 4: repeating tables, the part that bites
InfoPath’s repeating table doesn’t map onto a column. Build it as two lists: a parent list for the main record, and a child list with one row per line item and a lookup column back to the parent.
Bind a gallery in the parent form to the filtered child list:
Filter('Purchase Request Lines', ParentRequest.Id = SharePointForm1.Item.ID)
On save, write each row back with Patch inside a ForAll:
ForAll(
LinesGallery.AllItems,
Patch(
'Purchase Request Lines',
LookUp('Purchase Request Lines', ID = ThisRecord.ID),
{
ItemDescription: ThisRecord.ItemDescription,
Quantity: Value(ThisRecord.Quantity)
}
)
)
The LookUp inside ForAll has to key off ThisRecord.ID, the row the loop is currently on, and not a gallery’s .Selected property, which stays pinned to whatever the user last clicked. Get that wrong and every iteration patches the same record.
New rows need their own branch, since a blank ID means the row was never saved: wrap it in If(IsBlank(ThisRecord.ID), Patch(..., Defaults(...), {...}), Patch(...)). Deleted rows won’t clean themselves up either, so track what the user removed and Remove() those records in the same save action, or they sit orphaned.
Backfilling. Creating the new lists doesn’t move the old data. Reuse the extraction script, but instead of writing CSV, Add-PnPListItem each submission into the parent list, capture the returned item ID, and loop the repeating nodes into the child list with that ID as the lookup. Do it in a batch before cutover, then spot-check migrated records against the original XML.
Gotchas before you publish
- Quick Edit and grid view bypass the custom form entirely, including your validation.
- The mobile app renders the same customized form, but test it separately. Small screens expose layout problems browsers hide.
- “Return to default SharePoint form” is your rollback. It switches the list back to the out-of-the-box form immediately if a publish breaks something.
- Form versions are kept, so a bad publish can be rolled back rather than rebuilt.
- The attachments control has limits and doesn’t replicate every InfoPath attachment behaviour. Test with the real file types and sizes the form actually receives.
- Sharing differs by build type. A customized list form inherits list permissions. A standalone canvas app is its own object and has to be shared explicitly, or it opens for you and nobody else.
Licensing: what’s seeded and what isn’t
Customizing a SharePoint list form, and building canvas apps against SharePoint, Outlook, or Teams data, is covered by the standard Microsoft 365 seeded licence. No extra cost.
The moment a form uses a premium connector, connects to Dataverse, or goes through an on-premises data gateway, it moves into per-user or per-app Power Apps licensing. Check the connector list on every rebuilt form before go-live. The Power Platform admin centre flags premium connector usage, and a five-minute check is cheaper than explaining a licence bill at renewal.
Step 5: rebuild the workflow layer from evidence
If the form fed a SharePoint 2013 workflow or a Nintex approval, that layer needs rebuilding too, in Power Automate, and you can’t reverse-engineer it from memory.
For a SharePoint 2013 workflow, read the workflow history list on real items. It shows what actually fired, which is more reliable than the design view if the workflow was edited over the years. For Nintex, use Workflow Settings > Export to pull it as XML, which is faster to read than reopening the designer.
Either way, capture the same things: the trigger condition, who approves and how they’re resolved, escalation and reminder timings, what happens on rejection, the wording of any email templates, and which fields get written back on completion.
That list is your build spec. In Power Automate it becomes an automated cloud flow triggered on item creation, a Start and wait for an approval action, a condition on the outcome, and an update back to the item. A like-for-like approval rebuild uses only the SharePoint, Approvals, and Outlook connectors, all standard, so it stays inside your existing Microsoft 365 licensing. Approvers need no additional licence. The Power Automate rebuild post walks through that anatomy step by step.
One caveat on using the old workflow as a spec. The SharePoint 2013 workflow engine was itself retired from SharePoint Online on April 2, 2026, so if it stopped firing before anyone noticed, items may be sitting in an incomplete state and the history may be misleading. Confirm that before treating it as the source of truth. The workflow recovery post covers that side of it.
Bottom line
The order matters more than the tooling. Extract the form-library XML now, while the libraries are still there and still parseable, because nothing is going to render those submissions as forms again. Script the inventory instead of crawling sites. Sort what’s left with the rubric, and get a real decision on bucket (d) rather than forcing it into Power Apps.
Then rebuild, check delegation on anything with a repeating table, and check the connector list before go-live. The form that derails the project is usually one nobody had on the inventory at all, which is why the scripted sweep is worth the afternoon it costs.