PnP Provisioning: The Good, the Bad and the Ugly
You know that feeling when a Microsoft technology seems too good to be true? PnP Provisioning promised us the dream: extract a SharePoint site template, store it as XML, and deploy it anywhere with a single PowerShell command. After diving deep into production implementations, GitHub issues, and real-world failures, here's the truth about what actually happens when you bet your infrastructure on PnP provisioning.
The PnP Provisioning Engine is simultaneously one of the most powerful and most fragile tools in the SharePoint ecosystem.
It's brilliantly designed for simple scenarios, but the moment you need it to do real work—permissions, subsites, content migration—you'll discover why experienced developers have a love-hate relationship with it.
The Good: When PnP Provisioning Shines
1. Structural Provisioning Excellence
When you limit PnP to what it does best, it's genuinely impressive:
- Lists and Libraries: Creating document libraries, lists, and their configurations is rock-solid
- Content Types: Custom content types provision reliably (if you avoid syndication)
- Site Columns: Field definitions translate perfectly across sites
- Pages: Modern pages can be templated and deployed (on supported platforms)
2. Developer-Friendly Workflow
The basic workflow is elegant:
# Extract from template site
Get-PnPSiteTemplate -Out template.xml -Handlers Lists,ContentTypes,Fields
# Apply to new site
Invoke-PnPSiteTemplate -Path template.xml
3. Parameterization Power
The token system allows dynamic templates:
<List Title="{parameter:ProjectName}-Documents" />
Invoke-PnPSiteTemplate -Path template.xml `
-Parameters @{"ProjectName"="Momentum"}
4. Active Community
The PnP community is vibrant, with:
- 350+ organizations contributing to Gaia-X integration
- Active GitHub issue tracking and resolution
- Regular schema updates (current: V202209)
- Extensive documentation and examples
The Bad: Where It Gets Messy
1. Permission Provisioning is Broken
The Subsite Problem: Hard-coded in ObjectSiteSecurity.cs is a check that prevents any security settings from applying to subsites, even those with broken inheritance. This is intentional but undocumented.
// From source code analysis
if (web.IsSubSite()) {
// Skip all security provisioning!
return;
}
Permissions Are Additive: If a group already has Edit permissions and you apply a template with Contribute, it keeps Edit. Your template silently fails.
No Permission Removal: There's no XML schema for removing permissions. You must use PowerShell scripts as workarounds.
2. Performance Nightmares
Handlers That Hang:
ApplicationLifecycleManagement- Can hang indefinitelyImageRenditions- Causes multi-minute delaysNavigation- Slow and breaks on site URL changes
Solution: Always exclude these handlers:
Get-PnPSiteTemplate -Out template.xml `
-ExcludeHandlers ApplicationLifecycleManagement,ImageRenditions,Navigation
Large Site Timeouts: TEAMCHANNEL and SRCHCENTERLITE sites can take ~1 hour to extract, even when nearly empty.
VSCode Incompatibility: Scripts hang when run via VSCode PowerShell extension - use native PowerShell terminal instead.
3. Content Migration Doesn't Work As Advertised
Files Don't Copy: The provisioning engine intentionally doesn't export files from the source site. You must manually add them to the template XML.
List Items Require Cleanup: Exported list items include internal columns like ContentType that must be manually removed before import works.
Document Set Duplication Bug: Default documents in Document Set subfolders duplicate on every template run - 2 copies, 4 copies, 8 copies...
4. Schema Version Hell
Multiple active schemas create compatibility nightmares:
- V202209 (latest)
- V202103
- V202002 (required by some tools like Plumsail)
- Earlier versions still in use
Breaking changes in August 2020 invalidated thousands of existing templates.
The Ugly: The Real Pitfalls
1. Modern vs Classic Landmines
Security Model Conflict: Modern sites use Microsoft 365 Groups; classic sites use AD/SharePoint groups. Templates often fail silently when crossing this boundary.
ClientSidePages Ignored on SP2019: Modern pages simply don't provision on-premises - your template succeeds but pages vanish.
No Modern Subsites: Any subsite under a modern site must use classic templates. Modern subsite templates don't exist.
2. The Unique Permissions Trap
ObjectSecurity for items, PnP breaks inheritance anyway and creates unique permissions with no role assignments. Your carefully planned permission model? Destroyed.From GitHub issue #216:
"No check is done that there are actually any roleassignments specified before breaking the inheritance"
3. Default Group Names Are Variable
SharePoint's default groups (Owners, Members, Visitors) don't have fixed names. A template expecting "Site Owners" will fail on a site that has "ProjectX Owners".
Workaround: Use tokens:
<Group>{associatedownergroup}</Group>
4. Search Configuration Goes Stale
The Search handler frequently fails with:
"Bad request: Please export a new search configuration firstly"
Search configurations expire. Templates that worked yesterday fail today.
5. Navigation Breaks on Deployment
Navigation nodes with hard-coded URLs:
<NavigationNode Url="https://contoso.sharepoint.com/sites/template" />
These don't update when applied to a different site collection. Your navigation points to the template site.
Battle-Tested Best Practices
Template Extraction
# Enable detailed logging
Set-PnPTraceLog -On -Level Debug
# Selective extraction (faster + cleaner)
Get-PnPSiteTemplate -Out template.xml `
-Handlers Lists,ContentTypes,Fields,Files,WebSettings `
-ExcludeHandlers ApplicationLifecycleManagement,ImageRenditions,Navigation,SiteSecurity `
-ExcludeContentTypesFromSyndication
# Use configuration file for complex scenarios
Get-PnPSiteTemplate -Out template.xml -Configuration config.json
Template Application
# Test with specific handlers first
Invoke-PnPSiteTemplate -Path template.xml `
-Handlers Lists `
-Parameters @{
"SiteTitle"="Production Site"
"Owner"="admin@domain.com"
}
# Gradually add more handlers
Invoke-PnPSiteTemplate -Path template.xml `
-Handlers Lists,ContentTypes,Fields `
-ExcludeHandlers SiteSecurity `
-IgnoreDuplicateDataRowErrors
XML Template Editing
Always edit in Visual Studio with XSD validation:
<!-- Add proper namespace for IntelliSense -->
<pnp:Provisioning
xmlns:pnp="http://schemas.dev.office.com/PnP/2022/09/ProvisioningSchema"
Version="1.0">
<!-- Remove default attributes -->
<pnp:SiteColumn Name="CustomField"
DisplayName="Custom Field"
Type="Text"
Group="MyFields">
<!-- DON'T include: Required='FALSE' -->
<!-- DON'T include: FillInChoice='FALSE' -->
<!-- DON'T include: Version='X' -->
</pnp:SiteColumn>
</pnp:Provisioning>
Consecutive Template Strategy
For complex dependencies, use multiple templates:
# Template 1: Base structure (no dependencies)
Invoke-PnPSiteTemplate -Path template-base.xml
# Template 2: Dependent artifacts
Invoke-PnPSiteTemplate -Path template-advanced.xml
Handlers to ALWAYS Exclude
Never include these unless you absolutely need them:
$excludedHandlers = @(
'ApplicationLifecycleManagement', # Hangs
'ImageRenditions', # Slow
'Navigation', # Breaks on URL changes
'SiteSecurity', # Better handled manually
'SiteGroups', # Conflicts with custom security
'WebApiPermissions' # Rarely needed
)
Get-PnPSiteTemplate -Out template.xml `
-ExcludeHandlers ($excludedHandlers -join ',')
Real-World Production Strategy
Based on production implementation research:
What PnP Should Handle:
- ✅ Lists and libraries structure
- ✅ Content types (non-syndicated)
- ✅ Site columns
- ✅ Web settings
- ✅ Files (if manually added to XML)
What You Should Handle Manually:
- ❌ Permissions (use Azure AD groups via Microsoft Graph)
- ❌ Site security (custom PowerShell modules)
- ❌ Navigation (configure separately)
- ❌ Search (configure after provisioning)
Implementation Flow
# 1. Extract and cache template (one-time or triggered)
Get-PnPSiteTemplate -Out ./Configs/transactions_template.xml `
-Handlers Lists,ContentTypes,Fields,WebSettings `
-ExcludeHandlers SiteSecurity,Navigation,ApplicationLifecycleManagement
# 2. In provisioning workflow: Apply structure FIRST
if (Test-Path $templatePath) {
try {
Invoke-PnPSiteTemplate -Path $templatePath `
-ExcludeHandlers SiteSecurity `
-IgnoreDuplicateDataRowErrors `
-ErrorAction Stop
} catch {
Write-Warning "Template failed, continuing with permission config only"
# Don't let template failure block security provisioning
}
}
# 3. THEN apply custom permissions via Azure AD
Invoke-AzureAdGroupConfiguration -SiteUrl $siteUrl -Config $config
Invoke-SharePointPermissionsConfiguration -SiteUrl $siteUrl -Config $config
The Kill Switch
Critical: Never let PnP template failures block your core provisioning:
$SkipPnPProvisioning = $false # Config flag
if (-not $SkipPnPProvisioning -and (Test-Path $templatePath)) {
# Attempt PnP provisioning
} else {
Write-Host "Skipping PnP provisioning (disabled or template missing)"
}
# Always continue to permissions, regardless of PnP resultDescision Matrix
| Scenario | Verdict | Why |
|---|---|---|
| Repeatable site structures | YES | PnP excels at deploying identical lists/libraries multiple times |
| Dev to Production deployment | YES | Promotes configurations across environments reliably |
| Content type deployment | YES | Organization-wide content types work well |
| Simple modern sites (root only) | YES | No subsites = no permission headaches |
| Cached templates in source control | YES | Store once, deploy on demand |
| Complex permissions | NO | Broken inheritance, subsites, custom groups fail |
| Content migration | NO | Files don't copy - use dedicated migration tools |
| Large sites (1000+ items) | NO | Performance degrades, timeouts common |
| Cross-tenant scenarios | NO | Templates don't translate between tenants |
| Dynamic Azure AD security | NO | Use Microsoft Graph instead |
| Frequent permission changes | NO | Manual updates always faster |
| Hybrid: PnP structure + custom security | MAYBE | Works if you handle permissions separately |
| Classic sites | MAYBE | More stable than modern, but legacy concerns |
| Partial provisioning (specific handlers) | MAYBE | Use only Lists, Fields, ContentTypes |
| Template chains (sequential) | MAYBE | Requires careful dependency management |
Quick Reference Card
| Your Need | Solution |
|---|---|
| Site structure | PnP |
| Permissions | Microsoft Graph |
| Content migration | ShareGate/Migration Tool |
| Subsites | Custom PowerShell |
| Large sites | Custom code |
| Cross-tenant | Rebuild templates |
Use it for:
- Structural provisioning (lists, libraries, content types)
- Repeatable site templates
- Development workflow automation
Don't use it for:
- Permission management (use Microsoft Graph + Azure AD)
- Content migration (use migration tools)
- Complex subsites (manual configuration or custom code)
The Golden Rule:
"If your provisioning requirements include the word 'complex,' 'dynamic,' or 'permissions,' build custom PowerShell modules instead."
Action Items
For Developers Starting Out:
- Start small: Extract a simple template with only Lists and Fields
- Enable logging: Always use
Set-PnPTraceLog -On -Level Debug - Test in dev: Never run untested templates in production
- Build incrementally: Add one handler at a time
For Production Systems:
- Audit your handlers: Review what you're actually using
- Cache your templates: Store in source control, version them
- Build fallbacks: Don't let template failures block critical workflows
- Consider alternatives: Evaluate if custom PowerShell modules suit you better
For Decision Makers:
- Calculate TCO: Include template maintenance, debugging time, workarounds
- Assess complexity: If >3 handlers fail, consider custom development
- Plan exit strategy: Know how to provision without PnP if needed
Stay updated
Get new posts delivered to your inbox.