Live training: next batches coming soon. Join the waitlist:Waitlists open:Developer Training →Developer →Copilot Studio Training →Copilot Studio →

How to Create and Apply a SharePoint Site Template with PnP PowerShell

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:

New SharePoint site with the Project Tasks list and four tasks copied by a PnP site 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 / columnTypeNotes
Project TasksCustom list (GenericList, 100)Title plus the four columns below
Assigned To (AssignedTo)PersonWho owns the task
Due Date (DueDate)Date onlyDisplay format set to date only
Status (TaskStatus)ChoiceNot Started, In Progress, Completed; default Not Started
Priority (Priority)ChoiceHigh, Medium, Low
Project DocumentsDocument library (101)URL ProjectDocuments
Document Type (DocumentType)ChoiceProposal, Contract, Report, Meeting Notes
Review Date (ReviewDate)Date onlyWhen the document needs a review

And the four starter tasks every new project gets:

TaskDue DateStatusPriority
Kickoff meeting with stakeholders11/02/2026CompletedHigh
Gather requirements11/09/2026In ProgressHigh
Set up the project site11/16/2026Not StartedMedium
Send the weekly status report11/20/2026Not StartedLow

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 templateSharePoint site template (site design)
Built withPnP PowerShell (Get-PnPSiteTemplate)JSON site script + Add-SPOSiteDesign
Applied byAn admin running Invoke-PnPSiteTemplateUsers from the SharePoint UI, or automatically
File.xml or .pnpJSON stored in the tenant
List itemsYes, as data rowsNo
Best forCopying a real site you already builtSelf-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.

  1. Open PowerShell 7 (pwsh), not Windows PowerShell 5.1.
  2. If PnP PowerShell isn’t installed yet, remove the # in front of the Install-Module line and run it once.
  3. 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
PowerShell 7.6.7 console showing PnP.PowerShell module version 3.3.0

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.

  1. Run Register-PnPEntraIDAppForInteractiveLogin once for your tenant and sign in when the browser opens.
  2. Accept the consent prompt for the permissions the app asks for.
  3. 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.

  1. Stay connected to any site in the tenant (I used /sites/SPGuides).
  2. Run New-PnPSite with -Type CommunicationSite and -SiteDesign Blank.
  3. Leave -Wait on 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.

  1. Connect to the new source site.
  2. Create Project Tasks with New-PnPList, then add each column with Add-PnPField.
  3. Use Set-PnPField to make the dates date-only (DisplayFormat 0) and to give Status a default of Not Started.
  4. Create the Project Documents library with the URL ProjectDocuments and add its two columns.
  5. 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
VS Code running Build-SourceSite.ps1 to create the Project Tasks list and Project Documents library

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.

  1. Set $me to a user in your tenant (Assigned To needs a real account).
  2. Add the four tasks with Add-PnPListItem, using internal names as the keys.
  3. Upload one file, Project-Charter.docx, to Project Documents with Add-PnPFile and set its two columns.
  4. 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.

PowerShell output listing four sample project tasks with Status and Priority

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.

Source site Project Tasks list with four custom columns and four sample tasks

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

Source site Project Documents library with Document Type and Review Date columns and one file

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.

  1. Create a folder for templates (I used C:\SPGuidesDemo\Templates).
  2. Run Get-PnPSiteTemplate with -Handlers Lists and -ListsToExtract naming both lists.
  3. Run it a second time with a .pnp file name.
  4. 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
VS Code running Get-PnPSiteTemplate to export the site template as XML and PNP files

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:

ParameterWhat it does
-OutFile to write: .xml, .pnp (package with files) or .md (a readable report, still a work in progress)
-HandlersOnly process the named parts, for example Lists, Fields, ContentTypes, Pages, Navigation, Theme, SiteSecurity
-ListsToExtractExport only these lists, by title or ID
-IncludeAllPagesExport every page, not only the home page
-PersistBrandingFilesSave theme and branding files into the template
-IncludeSiteGroupsInclude the site’s SharePoint groups
-ConfigurationRead all export settings from a JSON configuration file
-ForceOverwrite 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.

  1. Load the .xml file into an [xml] variable.
  2. Walk down to Lists.ListInstance and print each list.
  3. 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
}
Console reading the site template XML and listing its lists and seven fields

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.

  1. Stay connected to the source site (the cmdlet reads the items from there).
  2. Run Add-PnPDataRowsToSiteTemplate against the .pnp file.
  3. 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 adding Project Tasks rows to the PNP template package

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.

  1. Create the target site, SPGuides Project Alpha, with New-PnPSite (again, give it 90 seconds or more).
  2. Connect to the new site’s URL. This line matters: Invoke-PnPSiteTemplate applies to whatever site you’re connected to.
  3. Run Invoke-PnPSiteTemplate with the .pnp path.
  4. 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
VS Code creating SPGuides Project Alpha and applying the template with Invoke-PnPSiteTemplate

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.

Target site Project Documents library with copied columns and no files

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
Second Invoke-PnPSiteTemplate run doubling Project Tasks to eight items

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:

  1. Load the .pnp file with Read-PnPSiteTemplate.
  2. Find the Project Tasks list in $template.Lists.
  3. Set DataRows.KeyColumn to Title and DataRows.UpdateBehavior to Overwrite.
  4. 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
Setting KeyColumn Title and UpdateBehavior Overwrite so re-applying keeps four tasks

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:

  1. Paste the function into your session (or a .ps1 file you dot-source).
  2. Call it with -ProjectName "Beta".
  3. 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
Reusable New-SPGuidesProjectSite function creating the SPGuides Project Beta site

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.

ParameterWhat it does
-PathThe .xml or .pnp file; a .pnp can also be a URL to a file in a document library
-ParametersHashtable of values for {parameter:Key} tokens in the template
-Handlers / -ExcludeHandlersApply only some parts, or everything except some parts
-ClearNavigationRemove the target site’s existing navigation nodes first
-IgnoreDuplicateDataRowErrorsSuppress errors when a data row already exists
-ResourceFolderFolder with files the template refers to (for .xml templates)
-TemplateIdPick 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.

ItemCopied?How
Lists, libraries, columns, viewsYesLists handler (default export)
List itemsOnly as data rowsAdd-PnPDataRowsToSiteTemplate on a .pnp file
Files in a libraryNo (tested: 0 files)Upload them after applying
PagesHome page by default-IncludeAllPages for the rest
Theme and branding filesTheme yes; files only on request-PersistBrandingFiles
SharePoint groupsOnly on request-IncludeSiteGroups
The site itselfNoCreate 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.com and 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).Url before 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

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.