Three new client projects landed on our PMO desk in one week, and each one wanted the same SharePoint site: a Project Tasks list with owners, due dates and status, plus a Project Documents library with a document type and review date. Building that by hand three times is an afternoon gone.
A PnP site template fixes it. You build the lists once on a model site, export them to a template file with PnP PowerShell, and apply that file to every new site in about a minute, sample tasks included.
In this tutorial, we’ll build a SharePoint site template PnP PowerShell workflow from scratch: install the module, create a source site, add the lists, export them with Get-PnPSiteTemplate, pack in sample rows, and apply everything to a brand-new site with Invoke-PnPSiteTemplate. Every command below ran in my tenant on 10/27/2026.
Here’s what we’re building. This is SPGuides Project Alpha, a site that didn’t exist a minute earlier, with the Project Tasks list, all four custom columns and the four starter tasks copied from the template:

The finished setup has four parts:
- A source site (/sites/SPGuidesProjectTemplate) that holds the model lists.
- A template file (ProjectSiteTemplate.pnp) exported from that site.
- Data rows inside the template, so every new site starts with the same tasks.
- A reusable function that creates a project site and applies the template in one call.
Before You Start
You need PowerShell 7.4 or later and the PnP.PowerShell module (Step 1 checks both). If you’ve never used it, my guide to install PnP PowerShell for SharePoint Online covers the setup in more detail.
- Permissions: creating sites with New-PnPSite needs the SharePoint Administrator or Global Administrator role, or self-service site creation turned on for your tenant.
- Entra ID app: since September 9, 2024, PnP PowerShell requires your own Entra ID app registration. Creating it needs rights to register apps and grant consent (Step 2).
- Tools: any editor works. I used VS Code for the longer scripts and a plain PowerShell 7 window for the short ones.
Here’s the list and library we’ll put in the template. Internal names matter, because the scripts and the data rows refer to them:
| List / column | Type | Notes |
|---|---|---|
| Project Tasks | Custom list (GenericList, 100) | Title plus the four columns below |
| Assigned To (AssignedTo) | Person | Who owns the task |
| Due Date (DueDate) | Date only | Display format set to date only |
| Status (TaskStatus) | Choice | Not Started, In Progress, Completed; default Not Started |
| Priority (Priority) | Choice | High, Medium, Low |
| Project Documents | Document library (101) | URL ProjectDocuments |
| Document Type (DocumentType) | Choice | Proposal, Contract, Report, Meeting Notes |
| Review Date (ReviewDate) | Date only | When the document needs a review |
And the four starter tasks every new project gets:
| Task | Due Date | Status | Priority |
|---|---|---|---|
| Kickoff meeting with stakeholders | 11/02/2026 | Completed | High |
| Gather requirements | 11/09/2026 | In Progress | High |
| Set up the project site | 11/16/2026 | Not Started | Medium |
| Send the weekly status report | 11/20/2026 | Not Started | Low |
How a PnP Site Template Works
A PnP site template is a file that describes a site: lists, columns, content types, views, pages, navigation, theme and more. Get-PnPSiteTemplate reads a live site and writes that file. Invoke-PnPSiteTemplate reads the file and creates whatever is missing on another site. It’s the open-source PnP provisioning engine, so there’s no Microsoft support SLA behind it.
Two rules shape everything that follows. First, a site template doesn’t create the site: the target site must already exist, which is why every apply step starts with New-PnPSite. Second, it copies structure, not content. List items only travel when you add them as data rows, and files in a library don’t travel at all with the export we use.
Note: SharePoint also has “site templates” in the browser (Settings > Apply a site template). Those are site designs built from JSON site scripts, which is a different tool.
If that’s what you want, read how to create a custom site template with site designs. To find which built-in template a site was created from, see how to get the site template ID with PowerShell.
| PnP site template | SharePoint site template (site design) | |
|---|---|---|
| Built with | PnP PowerShell (Get-PnPSiteTemplate) | JSON site script + Add-SPOSiteDesign |
| Applied by | An admin running Invoke-PnPSiteTemplate | Users from the SharePoint UI, or automatically |
| File | .xml or .pnp | JSON stored in the tenant |
| List items | Yes, as data rows | No |
| Best for | Copying a real site you already built | Self-service templates for site owners |
Step 1: Install PnP PowerShell and Check the Version
Goal: make sure the shell and module are new enough before we touch SharePoint.
- Open PowerShell 7 (pwsh), not Windows PowerShell 5.1.
- If PnP PowerShell isn’t installed yet, remove the
#in front of the Install-Module line and run it once. - Run the version checks.
# Run once: installs PnP PowerShell for your account (needs PowerShell 7.4 or later)
# Install-Module PnP.PowerShell -Scope CurrentUser
$PSVersionTable.PSVersion.ToString()
Get-Module PnP.PowerShell -ListAvailable |
Sort-Object Version -Descending |
Select-Object -First 1 Name, Version

Already have it? Run Update-Module PnP.PowerShell to get the latest release. If a later command says Connect-PnPOnline isn’t recognized, the module isn’t loaded in this shell; this fix for Connect-PnPOnline is not recognized walks through it.
Checkpoint: The console prints 7.6.7 for PowerShell and PnP.PowerShell 3.3.0 (any PowerShell 7.4+ and a current 3.x module works the same).
Step 2: Register Your Entra ID App and Connect
Goal: get a working sign-in that every later script reuses without asking for the client ID again.
- Run Register-PnPEntraIDAppForInteractiveLogin once for your tenant and sign in when the browser opens.
- Accept the consent prompt for the permissions the app asks for.
- Copy the client ID the command prints. That’s your app’s ID.
# Run once per tenant: creates your own Entra ID app for interactive PnP PowerShell sign-ins
# Replace yourtenant with your tenant name
Register-PnPEntraIDAppForInteractiveLogin -ApplicationName "PnP.PowerShell" -Tenant yourtenant.onmicrosoft.com
Now connect. Putting the client ID in $env:ENTRAID_CLIENT_ID means you can leave -ClientId off every Connect-PnPOnline call. -DeviceLogin shows a code you enter at the Microsoft sign-in page, and -PersistLogin caches the token so you don’t sign in again after a restart. There’s more on the options in my post on how to connect to SharePoint Online using PowerShell.
# Paste the client ID of the app you registered
$env:ENTRAID_CLIENT_ID = "<your app's client id>"
# Replace yourtenant with your tenant name
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/SPGuides" -DeviceLogin -PersistLogin
# Always confirm which site this window is connected to
(Get-PnPWeb).Url
Prefer a browser pop-up over a device code? Use -Interactive with -ClientId instead. The second line below is the fix I needed once when the device login refused to start:
# Alternative: sign in through a browser window instead of a device code
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/SPGuides" -Interactive -ClientId "<your app's client id>"
# If -DeviceLogin asks for the tenant, add it
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/SPGuides" -DeviceLogin -PersistLogin -Tenant yourtenant.onmicrosoft.com
Checkpoint: (Get-PnPWeb).Url returns https://yourtenant.sharepoint.com/sites/SPGuides, the site you just connected to.
Step 3: Create the Source Site
Goal: a clean communication site that will hold the model lists. Keeping the model on its own site means the template never picks up clutter from a real project.
- Stay connected to any site in the tenant (I used /sites/SPGuides).
- Run New-PnPSite with
-Type CommunicationSiteand-SiteDesign Blank. - Leave
-Waiton so the command returns only when the site is ready.
# Your Entra ID app's client ID is in $env:ENTRAID_CLIENT_ID, so -ClientId can be left out
# Replace yourtenant with your tenant name
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/SPGuides" -DeviceLogin -PersistLogin
# Create the source (model) site that we'll turn into a template
New-PnPSite -Type CommunicationSite `
-Title "SPGuides Project Template" `
-Url "https://yourtenant.sharepoint.com/sites/SPGuidesProjectTemplate" `
-SiteDesign Blank -Wait
Be patient here. With -Wait, New-PnPSite took more than 90 seconds for this site, and the window just sits there until SharePoint finishes. A team site works too, but -Type TeamSite needs -Alias and creates a Microsoft 365 group. My guide to create a site in SharePoint Online using PnP PowerShell compares both.
Checkpoint: The command returns https://yourtenant.sharepoint.com/sites/SPGuidesProjectTemplate and the empty site opens in the browser.
Step 4: Build the Project Tasks List and Project Documents Library
Goal: create the two lists and their columns on the source site, exactly as the Before You Start table describes.
- Connect to the new source site.
- Create Project Tasks with New-PnPList, then add each column with Add-PnPField.
- Use Set-PnPField to make the dates date-only (DisplayFormat 0) and to give Status a default of Not Started.
- Create the Project Documents library with the URL ProjectDocuments and add its two columns.
- List the results to confirm both exist.
# Replace yourtenant with your tenant name
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/SPGuidesProjectTemplate" -DeviceLogin -PersistLogin
# 1. Custom list: Project Tasks
New-PnPList -Title "Project Tasks" -Template GenericList -OnQuickLaunch | Out-Null
Add-PnPField -List "Project Tasks" -DisplayName "Assigned To" -InternalName "AssignedTo" -Type User -AddToDefaultView | Out-Null
Add-PnPField -List "Project Tasks" -DisplayName "Due Date" -InternalName "DueDate" -Type DateTime -AddToDefaultView | Out-Null
Set-PnPField -List "Project Tasks" -Identity "DueDate" -Values @{DisplayFormat = 0} # 0 = date only
Add-PnPField -List "Project Tasks" -DisplayName "Status" -InternalName "TaskStatus" -Type Choice `
-Choices "Not Started", "In Progress", "Completed" -AddToDefaultView | Out-Null
Set-PnPField -List "Project Tasks" -Identity "TaskStatus" -Values @{DefaultValue = "Not Started"}
Add-PnPField -List "Project Tasks" -DisplayName "Priority" -InternalName "Priority" -Type Choice `
-Choices "High", "Medium", "Low" -AddToDefaultView | Out-Null
# 2. Custom library: Project Documents
New-PnPList -Title "Project Documents" -Url "ProjectDocuments" -Template DocumentLibrary -OnQuickLaunch | Out-Null
Add-PnPField -List "Project Documents" -DisplayName "Document Type" -InternalName "DocumentType" -Type Choice `
-Choices "Proposal", "Contract", "Report", "Meeting Notes" -AddToDefaultView | Out-Null
Add-PnPField -List "Project Documents" -DisplayName "Review Date" -InternalName "ReviewDate" -Type DateTime -AddToDefaultView | Out-Null
Set-PnPField -List "Project Documents" -Identity "ReviewDate" -Values @{DisplayFormat = 0}
Get-PnPList | Where-Object Title -like "Project*" | Select-Object Title, ItemCount, BaseTemplate

The -InternalName values matter later: data rows and scripts use internal names, not display names. For more column types, see how to create a SharePoint list and add columns using PnP PowerShell, and these tips for customizing SharePoint list fields cover defaults and formats.
Checkpoint: The output table shows Project Documents with BaseTemplate 101 and Project Tasks with BaseTemplate 100, both with ItemCount 0.
Step 5: Add Sample Tasks and a Test Document
Goal: give the source site real data, so we can see exactly what the template carries over.
- Set
$meto a user in your tenant (Assigned To needs a real account). - Add the four tasks with Add-PnPListItem, using internal names as the keys.
- Upload one file, Project-Charter.docx, to Project Documents with Add-PnPFile and set its two columns.
- Read the tasks back.
# Replace yourtenant with your tenant name
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/SPGuidesProjectTemplate" -DeviceLogin -PersistLogin
$me = "bijay@yourtenant.onmicrosoft.com" # any user in your tenant
# Sample tasks every new project should start with (noon UTC keeps the date right in US time zones)
$tasks = @(
@{Title = "Kickoff meeting with stakeholders"; DueDate = "2026-11-02T12:00:00Z"; TaskStatus = "Completed"; Priority = "High"}
@{Title = "Gather requirements"; DueDate = "2026-11-09T12:00:00Z"; TaskStatus = "In Progress"; Priority = "High"}
@{Title = "Set up the project site"; DueDate = "2026-11-16T12:00:00Z"; TaskStatus = "Not Started"; Priority = "Medium"}
@{Title = "Send the weekly status report"; DueDate = "2026-11-20T12:00:00Z"; TaskStatus = "Not Started"; Priority = "Low"}
)
foreach ($t in $tasks) {
$t.AssignedTo = $me
Add-PnPListItem -List "Project Tasks" -Values $t | Out-Null
}
# One document in the library (to test what the template copies)
Add-PnPFile -Path ".\Project-Charter.docx" -Folder "ProjectDocuments" `
-Values @{DocumentType = "Proposal"; ReviewDate = "2026-11-06T12:00:00Z"} | Out-Null
Get-PnPListItem -List "Project Tasks" -Fields Title, TaskStatus, Priority |
ForEach-Object { [pscustomobject]@{ Task = $_["Title"]; Status = $_["TaskStatus"]; Priority = $_["Priority"] } }
Notice the dates end in T12:00:00Z. My first run used plain “2026-11-02” strings, and SharePoint showed 11/1/2026: a date-only string is stored as midnight UTC, which is still the previous day in a US time zone. Noon UTC lands on the right day everywhere in the US.

Here’s the list in the browser, with Assigned To, Due Date, Status and Priority filled in. For more ways to add items (CSV, batches, every column type), see how to add SharePoint Online list items using PnP PowerShell.

And the library with Document Type set to Proposal and a Review Date of 11/06/2026:

Checkpoint: Project Tasks has 4 items (Completed, In Progress and two Not Started) and Project Documents has 1 file, Project-Charter.docx.
Step 6: Export the Site Template with Get-PnPSiteTemplate
Goal: save the two lists to a template file, once as readable XML and once as a .pnp package.
- Create a folder for templates (I used C:\SPGuidesDemo\Templates).
- Run Get-PnPSiteTemplate with
-Handlers Listsand-ListsToExtractnaming both lists. - Run it a second time with a .pnp file name.
- Compare the two files.
# Replace yourtenant with your tenant name
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/SPGuidesProjectTemplate" -DeviceLogin -PersistLogin
$folder = "C:\SPGuidesDemo\Templates"
# Only the two custom lists, with their columns and views
$lists = "Project Tasks", "Project Documents"
# .xml = readable file, .pnp = package (needed for list data rows)
Get-PnPSiteTemplate -Out "$folder\ProjectSiteTemplate.xml" -Handlers Lists -ListsToExtract $lists -Force
Get-PnPSiteTemplate -Out "$folder\ProjectSiteTemplate.pnp" -Handlers Lists -ListsToExtract $lists -Force
Get-ChildItem "$folder\ProjectSiteTemplate.*" | Select-Object Name, Length, LastWriteTime

Why two formats? The .xml file is plain text you can read and edit. The .pnp file is a package (a zipped container) that can also hold files and data rows, and Add-PnPDataRowsToSiteTemplate in Step 8 only works with .pnp. In practice I keep the .xml for reading and apply the .pnp.
-Handlers Lists limits the export to lists and their columns and views. Without it, Get-PnPSiteTemplate also exports pages, navigation, theme and site settings. The docs warn that naming handlers “might result in a non-working template” when something depends on a skipped part, so test it, as we do in Step 9.
These export switches are the ones you’ll reach for most:
| Parameter | What it does |
|---|---|
| -Out | File to write: .xml, .pnp (package with files) or .md (a readable report, still a work in progress) |
| -Handlers | Only process the named parts, for example Lists, Fields, ContentTypes, Pages, Navigation, Theme, SiteSecurity |
| -ListsToExtract | Export only these lists, by title or ID |
| -IncludeAllPages | Export every page, not only the home page |
| -PersistBrandingFiles | Save theme and branding files into the template |
| -IncludeSiteGroups | Include the site’s SharePoint groups |
| -Configuration | Read all export settings from a JSON configuration file |
| -Force | Overwrite the file if it already exists |
If you want the whole site instead of two lists, drop -Handlers and -ListsToExtract and add the switches you need:
# Everything the default handlers export (pages, navigation, theme, lists, columns ...)
Get-PnPSiteTemplate -Out "C:\SPGuidesDemo\Templates\FullSite.pnp" -IncludeAllPages -PersistBrandingFiles -IncludeSiteGroups -Force
Checkpoint: ProjectSiteTemplate.xml is 7,949 bytes and ProjectSiteTemplate.pnp is 4,922 bytes.
Step 7: Look Inside the Template File
Goal: confirm the template holds what we expect before we apply it anywhere. Two minutes here saves a confused hour later.
- Load the .xml file into an
[xml]variable. - Walk down to Lists.ListInstance and print each list.
- Print every field inside each list.
# Read the exported template and list what it will create
[xml]$template = Get-Content "C:\SPGuidesDemo\Templates\ProjectSiteTemplate.xml"
$lists = $template.Provisioning.Templates.ProvisioningTemplate.Lists.ListInstance
$lists | Select-Object Title, Url, TemplateType | Format-Table
foreach ($list in $lists) {
$list.Fields.Field | Select-Object @{n = "List"; e = { $list.Title }}, DisplayName, Name, Type
}

There’s one surprise: a seventh field, Checked Out To (CheckoutUser), which SharePoint adds to every document library on its own. It’s harmless.
The template records each list’s TemplateType (100 and 101) and URL, and the file uses the 2022/09 schema with SITEPAGEPUBLISHING#0 as its base template. To read the same details from a live site, see how to get SharePoint list column details using PnP PowerShell.
Checkpoint: Two lists (Project Documents, Project Tasks) and seven fields: Document Type, Review Date, Checked Out To, Assigned To, Due Date, Status and Priority.
Step 8: Add the Sample Tasks to the Template as Data Rows
Goal: make every new site start with the four tasks, not an empty list.
- Stay connected to the source site (the cmdlet reads the items from there).
- Run Add-PnPDataRowsToSiteTemplate against the .pnp file.
- Name the list and the internal names of the fields to copy.
# Replace yourtenant with your tenant name
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/SPGuidesProjectTemplate" -DeviceLogin -PersistLogin
# Copy the 4 sample tasks into the .pnp package (works with .pnp files only)
Add-PnPDataRowsToSiteTemplate -Path "C:\SPGuidesDemo\Templates\ProjectSiteTemplate.pnp" `
-List "Project Tasks" -Fields "Title", "AssignedTo", "DueDate", "TaskStatus", "Priority"
Get-Item "C:\SPGuidesDemo\Templates\ProjectSiteTemplate.pnp" | Select-Object Name, Length, LastWriteTime

Add-PnPDataRowsToSiteTemplate also takes -Query (a CAML query) to copy only some items, -IncludeSecurity for item permissions, and -TokenizeUrls to swap site URLs for tokens. The rows land with an empty KeyColumn and UpdateBehavior Overwrite, which matters in the test below. To edit or remove rows on a live site instead, see SharePoint list item CRUD with PnP PowerShell.
Checkpoint: The .pnp file grows from 4,922 to 5,187 bytes and now contains 4 data rows for Project Tasks.
Step 9: Create a New Site and Apply the Template with Invoke-PnPSiteTemplate
Goal: the payoff. A new project site gets both lists, all columns and the starter tasks.
- Create the target site, SPGuides Project Alpha, with New-PnPSite (again, give it 90 seconds or more).
- Connect to the new site’s URL. This line matters: Invoke-PnPSiteTemplate applies to whatever site you’re connected to.
- Run Invoke-PnPSiteTemplate with the .pnp path.
- Count the items in both lists.
# 1. Create the empty target site (replace yourtenant with your tenant name)
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/SPGuides" -DeviceLogin -PersistLogin
$newSite = New-PnPSite -Type CommunicationSite -Title "SPGuides Project Alpha" `
-Url "https://yourtenant.sharepoint.com/sites/SPGuidesProjectAlpha" -SiteDesign Blank -Wait
"Created: $newSite"
# 2. Connect to the new site and apply the template
Connect-PnPOnline -Url $newSite -DeviceLogin -PersistLogin
Invoke-PnPSiteTemplate -Path "C:\SPGuidesDemo\Templates\ProjectSiteTemplate.pnp"
# 3. Check the result
Get-PnPList | Where-Object Title -like "Project*" | Select-Object Title, ItemCount

Project Tasks arrived with all four tasks. Project Documents arrived with Document Type and Review Date, but no files. That’s expected: the Lists handler exports list structure and data rows, not library documents.

If new projects also need starter documents, upload them after the apply step. My guide to upload files to a SharePoint document library using PnP PowerShell has a ready loop for a whole folder, and you can pull the originals off the model site with a script to download all files from a document library.
Checkpoint: The output shows Created: https://yourtenant.sharepoint.com/sites/SPGuidesProjectAlpha, Project Documents 0 items and Project Tasks 4 items.
Test the Template: Apply It Twice to the Same Site
Real templates get re-applied: someone adds a column to the model, and you push the update to existing sites. So let’s run the exact same Invoke against Alpha a second time.
# Replace yourtenant with your tenant name
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/SPGuidesProjectAlpha" -DeviceLogin -PersistLogin
# Apply the same template a second time
Invoke-PnPSiteTemplate -Path "C:\SPGuidesDemo\Templates\ProjectSiteTemplate.pnp"
Get-PnPList | Where-Object Title -like "Project*" | Select-Object Title, ItemCount

The lists and columns were fine, because the engine skips what already exists. The data rows weren’t: Project Tasks went from 4 to 8 items, with every task twice. With an empty KeyColumn, the engine has no way to tell that a row is already there, so it adds it again.
Important: never re-apply a template that has data rows to a live site until you’ve set a key column, or every run adds another copy of the starter tasks.
Stop Duplicate Rows With KeyColumn and UpdateBehavior
Same template, one change: tell the data rows which column identifies a row. I removed the four extra tasks first, then ran this:
- Load the .pnp file with Read-PnPSiteTemplate.
- Find the Project Tasks list in
$template.Lists. - Set
DataRows.KeyColumnto Title andDataRows.UpdateBehaviorto Overwrite. - Write it back with Save-PnPSiteTemplate, then apply it again.
$path = "C:\SPGuidesDemo\Templates\ProjectSiteTemplate.pnp"
# Tell the engine how to recognize a row that already exists
$template = Read-PnPSiteTemplate -Path $path
$tasks = $template.Lists | Where-Object Title -eq "Project Tasks"
$tasks.DataRows.KeyColumn = "Title" # match rows on the Title column
$tasks.DataRows.UpdateBehavior = "Overwrite" # update the match instead of adding a copy
Save-PnPSiteTemplate -Template $template -Out $path -Force
# Apply it again to the same site: still 4 tasks, no duplicates (replace yourtenant)
Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/SPGuidesProjectAlpha" -DeviceLogin -PersistLogin
Invoke-PnPSiteTemplate -Path $path
Get-PnPList | Where-Object Title -like "Project*" | Select-Object Title, ItemCount

Now the engine matches each row on Title. Overwrite means a match gets updated, not copied.
In my re-apply, the existing tasks took the Due Date values from the template, and no copies were added. Pick a column whose values are unique in that list. If you ever need to clean up duplicates by hand, see how to delete all items from a SharePoint list using PnP PowerShell.
Checkpoint: After the second apply, Project Tasks still has 4 items.
Make It Reusable: One Command per New Project Site
Typing three commands for every project gets old. Wrap them in a function that takes the project name, builds the URL, creates the site, applies the template and reports back:
- Paste the function into your session (or a .ps1 file you dot-source).
- Call it with
-ProjectName "Beta". - Read the summary it prints.
function New-SPGuidesProjectSite {
param(
[Parameter(Mandatory)] [string] $ProjectName, # e.g. "Beta"
[string] $TemplatePath = "C:\SPGuidesDemo\Templates\ProjectSiteTemplate.pnp",
[string] $Tenant = "https://yourtenant.sharepoint.com" # replace yourtenant
)
$url = "$Tenant/sites/SPGuidesProject$ProjectName"
Connect-PnPOnline -Url "$Tenant/sites/SPGuides" -DeviceLogin -PersistLogin
$site = New-PnPSite -Type CommunicationSite -Title "SPGuides Project $ProjectName" `
-Url $url -SiteDesign Blank -Wait
Connect-PnPOnline -Url $site -DeviceLogin -PersistLogin
Invoke-PnPSiteTemplate -Path $TemplatePath
$lists = Get-PnPList | Where-Object Title -like "Project*"
[pscustomobject]@{ Site = $site; Lists = ($lists.Title -join ", "); Tasks = ($lists | Where-Object Title -eq "Project Tasks").ItemCount }
}
New-SPGuidesProjectSite -ProjectName "Beta" | Format-List

That one call created /sites/SPGuidesProjectBeta with Project Documents and Project Tasks and the 4 starter tasks. To check later which sites you’ve created this way, list them with the script that shows how to get all SharePoint Online sites using PnP PowerShell.
Invoke-PnPSiteTemplate Options Worth Knowing
Our build only needed -Path, but these switches cover most of what people ask about. The cheat sheet of SharePoint PowerShell commands for SPO and PnP lists the related cmdlets.
| Parameter | What it does |
|---|---|
| -Path | The .xml or .pnp file; a .pnp can also be a URL to a file in a document library |
| -Parameters | Hashtable of values for {parameter:Key} tokens in the template |
| -Handlers / -ExcludeHandlers | Apply only some parts, or everything except some parts |
| -ClearNavigation | Remove the target site’s existing navigation nodes first |
| -IgnoreDuplicateDataRowErrors | Suppress errors when a data row already exists |
| -ResourceFolder | Folder with files the template refers to (for .xml templates) |
| -TemplateId | Pick one template when the file contains several |
Parameters let one template serve many projects. Put a token in the XML, then pass the value when you apply it:
# In the template XML, a value can be a token such as {parameter:ProjectName}
Invoke-PnPSiteTemplate -Path "C:\SPGuidesDemo\Templates\ProjectSiteTemplate.xml" -Parameters @{"ProjectName" = "Alpha"}
What a PnP Site Template Copies (and What It Doesn’t)
From the tests above and the docs, here’s the short version. If you only need to move one list between sites, the copy items from one list to another with PnP PowerShell approach is lighter than a template.
| Item | Copied? | How |
|---|---|---|
| Lists, libraries, columns, views | Yes | Lists handler (default export) |
| List items | Only as data rows | Add-PnPDataRowsToSiteTemplate on a .pnp file |
| Files in a library | No (tested: 0 files) | Upload them after applying |
| Pages | Home page by default | -IncludeAllPages for the rest |
| Theme and branding files | Theme yes; files only on request | -PersistBrandingFiles |
| SharePoint groups | Only on request | -IncludeSiteGroups |
| The site itself | No | Create it first, or use a tenant template |
That last row is the big one. A site template applies to an existing site. A tenant template can also create site collections through a Sequence and is applied with Invoke-PnPTenantTemplate, which is the route for provisioning many sites from one file.
Make It Production-Ready
- Keep the template in source control. The .xml diff shows exactly what changed between versions.
- Set a KeyColumn on every list with data rows before anyone re-applies the template.
- Check the connection before every apply. Print
(Get-PnPWeb).Url; a script that checks whether a site or list exists first is safer still. - Test on a throwaway site. Apply to a new test site, check the lists, then delete it.
- Name sites consistently (SPGuidesProject + name), so the function builds predictable URLs.
Troubleshooting
These are the problems I actually hit while building this, with the fix for each:
- “Default authentication request failed. Please specify the -Tenant parameter”: Connect-PnPOnline -DeviceLogin couldn’t work out the tenant. Add
-Tenant yourtenant.onmicrosoft.comand it connects. - The template went to the wrong site: when a Connect-PnPOnline call fails, the PREVIOUS connection stays active, so the next Invoke runs against the old site. Always check
(Get-PnPWeb).Urlbefore applying. - Due dates one day early: “2026-11-02” is stored as midnight UTC, which a US time-zone site shows as 11/1/2026. Write “2026-11-02T12:00:00Z” (noon UTC) instead.
- New-PnPSite seems frozen: with -Wait it can take more than 90 seconds. Let it finish; don’t start the apply step early.
- Duplicate list items after re-applying: set DataRows.KeyColumn and UpdateBehavior Overwrite (see above).
- Library arrives empty: templates don’t carry documents. Upload the files after applying.
Note: if Connect-PnPOnline still fails after adding -Tenant, go back to the app registration from Step 2. The app needs consent before anyone can sign in through it.
Where to Take It Next
- Add a document library structure: extend the model with folders using the steps to create a document library with folders using PnP PowerShell, then export again.
- Let site owners apply it themselves: for a no-PowerShell option, rebuild the lists as a site design (linked above), and browse the SharePoint tutorials hub for the site setup guides around it.
- Clean up test sites: remove the throwaway sites you created while testing with the script to delete a SharePoint Online site using PowerShell.
If you’re automating more of your SharePoint setup, these PnP PowerShell guides pair well with this one:
Frequently Asked Questions
Does Get-PnPSiteTemplate copy the files in a document library?
No. In my test the target library got the Document Type and Review Date columns but 0 files. Templates carry structure, and list items only as data rows, so upload documents with Add-PnPFile after applying the template.
Should I save the template as .xml or .pnp?
Use .xml when you want to read or edit the template, and .pnp when you need data rows or files inside it. Add-PnPDataRowsToSiteTemplate only works with .pnp files, so I apply the .pnp.
Why do I get duplicate list items when I apply the template again?
The data rows have no key column, so the engine can’t tell a row already exists. Set DataRows.KeyColumn to a unique column such as Title and UpdateBehavior to Overwrite, then save the template with Save-PnPSiteTemplate.
Is a PnP site template the same as a SharePoint site template in the browser?
No. The browser’s site templates are site designs built from JSON site scripts. A PnP site template is an .xml or .pnp file that PnP PowerShell exports from a real site and applies with Invoke-PnPSiteTemplate.
Can a PnP template create the site for me?
A site template can’t: the target site must exist first. A tenant template can create site collections through a Sequence and is applied with Invoke-PnPTenantTemplate. Microsoft explains both in Introducing the PnP provisioning engine.

Hey! I’m Bijay Kumar, founder of SPGuides.com and a Microsoft Business Applications MVP (Power Automate, Power Apps). I launched this site in 2020 because I truly enjoy working with SharePoint, Power Platform, and SharePoint Framework (SPFx), and wanted to share that passion through step-by-step tutorials, guides, and training videos. My mission is to help you learn these technologies so you can utilize SharePoint, enhance productivity, and potentially build business solutions along the way.