When working with repeating section data from O365 and migrating to NW, there are some best practices and considerations that should be taken.
The Challenge
- Repeating Sections in Nintex Forms for Office 365 store their values in XML, not in normal SharePoint columns.
- Unless a Repeating Section is bound to a multi-line text field, its content is hidden inside a special internal field (NFFormData/FormData).
- This makes the data hard to report on or export directly.
Why This Matters
- Customers often need historical access to repeating section data (for audits, reports, or migration).
- Without action, this data remains locked in XML and is only visible inside the form.
Options to Access the Data
- Best Practice for New Forms
- Connect the Repeating Section to a Plain Text multi-line column.
- This ensures all future entries are stored in a standard SharePoint column.
- For Existing Data
- Use a workflow (Query XML action) to parse Form Data.
- Or, use PowerShell (PnP.PowerShell) to pull repeating section XML from the hidden NFFormData field.
- Extracted data can be saved into a new multi-line text column for easier access.
Recommended Solution for Historical Data
- Run a PowerShell script that:
- Connects to the SharePoint list.
- Reads each item’s NFFormData value.
- Extracts the Repeating Section data.
- Writes that data into a new multi-line text column (e.g., “RepeatingSectionArchive”).
- This approach ensures:
✅ Historical data is preserved in a visible column.
✅ Reports, exports, and audits can use standard list fields.
✅ Future form submissions can continue writing directly to the archive column (if connected).
Benefits to You
- Transparency: All Repeating Section content is visible in SharePoint.
- Control: You can filter, sort, and report on this data like any other field.
- Futureproofing: Once set up, no more hidden XML headaches.
🔑 Key Takeaway:
- For new forms → connect Repeating Sections to a multi-line text column.
- For existing forms → run a PowerShell script to backfill and unlock historical data.
Plain Text
# 1) Install PnP if you don’t have it
Install-Module PnP.PowerShell -Scope CurrentUser
# 2) Run the script with your settings
.\NintexRepeaterArchive.ps1 `
-SiteUrl "https://contoso.sharepoint.com/sites/YourSite" `
-ListTitle "Requests" `
-TargetXmlField "RepeaterArchive" `
-RepeaterXPaths "//RepeatingSection/Item" `
-AlsoStoreJson $true `
-TargetJsonField "RepeaterArchiveJson"
Parameters you’ll set
-SiteUrl – your SPO site URL.
-ListTitle – the list with the Nintex items.
-TargetXmlField – internal name for the Plain Text multi-line column to store the XML
Full Script and details:
Plain Text
<#
.SYNOPSIS
Backfill Nintex for O365 Repeating Section data into SharePoint columns.
.DESCRIPTION
Reads unbound Nintex form XML from the hidden NFFormData/FormData field for each list item,
extracts one or more Repeating Section blocks by XPath, and writes results to multi-line
text fields (XML and/or JSON) on the same item.
.REQUIREMENTS
- PnP.PowerShell module (Install-Module PnP.PowerShell -Scope CurrentUser)
- Permission to read and update items on the target list
.EXAMPLE
.\NintexRepeaterArchive.ps1 `
-SiteUrl "https://contoso.sharepoint.com/sites/Finance" `
-ListTitle "Expense Requests" `
-TargetXmlField "RepeaterArchive" `
-RepeaterXPaths "//RepeatingSection/Item" `
-AlsoStoreJson $true `
-TargetJsonField "RepeaterArchiveJson"
#>
param(
[Parameter(Mandatory=$true)][string]$SiteUrl,
[Parameter(Mandatory=$true)][string]$ListTitle,
# Destination (must be Note/Plain Text). Created automatically if missing.
[Parameter(Mandatory=$true)][string]$TargetXmlField,
# One or more XPath expressions that locate the <Item> nodes inside each Repeating Section.
# Example: "//RepeatingSection/Item", or "//MyRepeater/Item"
[Parameter(Mandatory=$true)][string[]]$RepeaterXPaths,
# Optionally also store a flattened JSON array of the rows.
[bool]$AlsoStoreJson = $false,
[string]$TargetJsonField = "RepeaterArchiveJson",
# Optional: CAML query to limit which items are processed (e.g., only items whose archive field is empty).
[string]$CamlQuery = $null,
# Performance knobs
[int]$PageSize = 500,
[int]$SleepMsBetweenItems = 0,
# Safety
[switch]$WhatIf
)
function Ensure-PlainTextNoteField {
param([string]$ListTitle,[string]$InternalName)
$f = Get-PnPField -List $ListTitle -Identity $InternalName -ErrorAction SilentlyContinue
if (-not $f) {
Write-Host "Creating multi-line text field '$InternalName' on list '$ListTitle'..." -ForegroundColor Cyan
Add-PnPField -List $ListTitle -DisplayName $InternalName -InternalName $InternalName -Type Note -AddToDefaultView | Out-Null
# enforce Plain Text
Set-PnPField -List $ListTitle -Identity $InternalName -Values @{ RichText = $false } | Out-Null
} elseif ($f.TypeAsString -ne "Note") {
throw "Field '$InternalName' exists but is not a Note (multi-line) field."
} else {
# ensure Plain Text
Set-PnPField -List $ListTitle -Identity $InternalName -Values @{ RichText = $false } | Out-Null
}
}
function Get-FormXmlFromItem {
param($ListItem)
# NFFormData is preferred; fallback to FormData
$raw = $ListItem["NFFormData"]
if (-not $raw) { $raw = $ListItem["FormData"] }
if ([string]::IsNullOrWhiteSpace($raw)) { return $null }
Add-Type -AssemblyName System.Web
$decoded = [System.Web.HttpUtility]::HtmlDecode($raw)
try {
[xml]$xml = $decoded
return $xml
} catch {
throw "Item ID $($ListItem['ID']): Failed to parse form XML. $_"
}
}
function Select-RepeaterRows {
param([xml]$FormXml,[string[]]$XPaths)
foreach ($xp in $XPaths) {
try {
$nodes = $FormXml.SelectNodes($xp)
if ($nodes -and $nodes.Count -gt 0) {
return ,$nodes # return immediately on first match set
}
} catch {
Write-Verbose "XPath error for '$xp': $_"
}
}
return @()
}
function Flatten-ItemNodeToHashtable {
param([System.Xml.XmlNode]$Node)
$map = @{}
foreach ($child in $Node.ChildNodes) {
if ($child.NodeType -eq [System.Xml.XmlNodeType]::Element) {
# If duplicate names occur, suffix with an index
$name = $child.Name
$val = $child.InnerText
if ($map.ContainsKey($name)) {
$i = 2
while ($map.ContainsKey("$name`_$i")) { $i++ }
$name = "$name`_$i"
}
$map[$name] = $val
}
}
return $map
}
# --- Connect ---
Connect-PnPOnline -Url $SiteUrl -Interactive
# --- Ensure destination fields ---
Ensure-PlainTextNoteField -ListTitle $ListTitle -InternalName $TargetXmlField
if ($AlsoStoreJson) { Ensure-PlainTextNoteField -ListTitle $ListTitle -InternalName $TargetJsonField }
# --- Pull items ---
$fieldsToLoad = @("ID","NFFormData","FormData",$TargetXmlField)
if ($AlsoStoreJson) { $fieldsToLoad += $TargetJsonField }
if ($CamlQuery) {
$items = Get-PnPListItem -List $ListTitle -Query $CamlQuery -PageSize $PageSize -Fields $fieldsToLoad
} else {
$items = Get-PnPListItem -List $ListTitle -PageSize $PageSize -Fields $fieldsToLoad
}
$updated = 0; $skippedNoFormData = 0; $skippedNoRows = 0; $unchanged = 0; $errors = 0
foreach ($it in $items) {
try {
$id = $it["ID"]
$formXml = Get-FormXmlFromItem -ListItem $it
if (-not $formXml) { $skippedNoFormData++; continue }
$rows = Select-RepeaterRows -FormXml $formXml -XPaths $RepeaterXPaths
if (-not $rows -or $rows.Count -eq 0) { $skippedNoRows++; continue }
# Compose outputs
$xmlOut = ($rows | ForEach-Object { $_.OuterXml }) -join "`r`n"
$vals = @{}; $changed = $false
if ([string]::IsNullOrWhiteSpace($it[$TargetXmlField]) -or $it[$TargetXmlField] -ne $xmlOut) {
$vals[$TargetXmlField] = $xmlOut
$changed = $true
}
if ($AlsoStoreJson) {
$jsonObjs = foreach ($r in $rows) { [pscustomobject](Flatten-ItemNodeToHashtable -Node $r) }
$jsonOut = $jsonObjs | ConvertTo-Json -Depth 10
if ([string]::IsNullOrWhiteSpace($it[$TargetJsonField]) -or $it[$TargetJsonField] -ne $jsonOut) {
$vals[$TargetJsonField] = $jsonOut
$changed = $true
}
}
if ($changed) {
if ($WhatIf) {
Write-Host "[WhatIf] Would update item $id" -ForegroundColor Yellow
} else {
Set-PnPListItem -List $ListTitle -Identity $id -Values $vals | Out-Null
Write-Host "Updated item $id" -ForegroundColor Green
}
$updated++
} else {
$unchanged++
}
if ($SleepMsBetweenItems -gt 0) { Start-Sleep -Milliseconds $SleepMsBetweenItems }
}
catch {
Write-Warning "Item $($it['ID']) error: $_"
$errors++
}
}
Write-Host "Done." -ForegroundColor Cyan
Write-Host ("Updated: {0} Unchanged: {1} Skipped (no form data): {2} Skipped (no rows): {3} Errors: {4}" -f `
$updated, $unchanged, $skippedNoFormData, $skippedNoRows, $errors)
This is a self-contained, production-ready PnP.PowerShell script that backfills Nintex O365 Repeating Section data from the hidden NFFormData/FormData into multi-line text columns (XML and optional JSON). Copy it into NintexRepeaterArchive.ps1 and run with the example above.
Notes
- Make sure your destination fields are Plain Text (the script enforces this).
- If your form uses named sections, adjust -RepeaterXPaths (e.g., //MyRepeater/Item).
