A few weeks back, I was working on a SharePoint Online migration project for a client in the manufacturing space. They had close to 40 department sites, and every single one of them needed the same thing – a document library called “Training Documents” with a fixed set of department folders inside it (HR, Finance, IT, Sales, Marketing, and Projects).
Doing this manually through the SharePoint UI would have meant roughly 280 clicks and a very long, very boring afternoon. So I did what I always do in these situations – I opened up PowerShell and wrote a PnP PowerShell script that finished the entire job in under two minutes.
In this tutorial, I’ll walk you through exactly how to create a document library in SharePoint Online using PnP PowerShell, and then create multiple folders inside it. I’ll show you several methods – from a simple hardcoded array to reading folder names from a CSV file, creating nested subfolders, and even doing it across multiple sites at once.
I’ll also point out a few mistakes I see people make in their scripts (including one in the “normal script” most people start with), so you don’t run into the same errors I did.
Let’s get into it.
What You Need Before You Start
Before running any of the scripts in this tutorial, make sure you have these three things in place.
1. PnP PowerShell module installed
If you haven’t installed it yet, run this command in an elevated PowerShell window:
Install-Module PnP.PowerShell -Scope CurrentUser -Force
To confirm the installation worked:
Get-Module PnP.PowerShell -ListAvailable | Select-Object Name, Version
2. An Entra ID (Azure AD) App Registration with a Client ID
This is important, and it trips up a lot of people following older tutorials.
Since September 2024, PnP PowerShell no longer ships with a built-in multi-tenant app registration. That means you must pass a -ClientId when you connect, otherwise the connection will simply fail.
If you don’t have one yet, the quickest way to create it is:
Register-PnPEntraIDAppForInteractiveLogin `
-ApplicationName "PnP PowerShell Admin App" `
-Tenant "tsinfotechnologies.onmicrosoft.com" `
-Interactive
This will create the app registration in your tenant and return a Client ID. Copy that GUID and save it somewhere – you’ll be reusing it in every script.
3. Permissions
You need at least Site Owner (or Full Control) permission on the site where you’re creating the library. Site Members can’t create SharePoint lists or libraries.
Method 1: Create a Document Library and Multiple Folders (The Basic Script)
Let’s start with the version most people write first, and then I’ll show you a production-ready script.
Here’s the “normal script” I see all the time:
$ClientID = "655a839c-8659-AAAAAAAAAAAA"
$SiteUrl = "https://tsinfotechnologies.sharepoint.com/sites/PnPPowerShell"
Connect-PnPOnline -Url $SiteUrl -ClientId $ClientID
New-PnPList -Title "Training Documents" -Template DocumentLibrary
$Folders = @("HR","Finance","IT","Sales","Marketing","Projects")
foreach($Folder in $Folders)
{
Add-PnPFolder -Name $Folder -Folder "Training Documents"
}
At first glance, this looks fine. And honestly, it will work most of the time. But there are three real problems with it that I ran into on the client project.
I executed the above PowerShell script, and you can see the output in the screenshot below:

Here is the production ready script that you can use:
# ---- Configuration ----
$ClientID = "655a839c-8659-AAAAAAAAAAAA"
$SiteUrl = "https://tsinfotechnologies.sharepoint.com/sites/PnPPowerShell"
$LibraryName = "Training Documents"
$Folders = @("HR","Finance","IT","Sales","Marketing","Projects")
# ---- Connect ----
Connect-PnPOnline -Url $SiteUrl -ClientId $ClientID -Interactive
# ---- Create the document library only if it doesn't exist ----
$Library = Get-PnPList -Identity $LibraryName -ErrorAction SilentlyContinue
if ($null -eq $Library) {
$Library = New-PnPList -Title $LibraryName `
-Template DocumentLibrary `
-OnQuickLaunch
Write-Host "Created document library: $LibraryName" -ForegroundColor Green
}
else {
Write-Host "Library '$LibraryName' already exists. Skipping creation." -ForegroundColor Yellow
}
# ---- Get the real root folder URL of the library ----
$LibraryUrl = $Library.RootFolder.ServerRelativeUrl
if (-not $LibraryUrl) {
$LibraryUrl = (Get-PnPList -Identity $LibraryName -Includes RootFolder).RootFolder.ServerRelativeUrl
}
# ---- Create the folders ----
foreach ($Folder in $Folders) {
try {
Add-PnPFolder -Name $Folder -Folder $LibraryUrl -ErrorAction Stop
Write-Host " Created folder: $Folder" -ForegroundColor Green
}
catch {
Write-Host " Folder '$Folder' could not be created: $($_.Exception.Message)" -ForegroundColor Red
}
}
Disconnect-PnPOnline
Let me break down what each part does.
Get-PnPList -Identity $LibraryName -ErrorAction SilentlyContinue – This checks whether the library already exists in the SharePoint site. Using -ErrorAction SilentlyContinue means the script won’t blow up if it isn’t found; it just returns $null.
-OnQuickLaunch – I add this because clients almost always ask “why can’t I see it in the left navigation?” afterwards. This switch adds the library to the left-hand navigation automatically.
$Library.RootFolder.ServerRelativeUrl – This gives you the exact internal path, something like /sites/PnPPowerShell/Training Documents. This is what Add-PnPFolder actually wants.
The try/catch block – If one folder fails (say, because it already exists), the loop keeps going instead of stopping dead.
Method 2: Create Folders and Skip the Ones That Already Exist
On my client project, I had to run the script multiple times as they kept adding new department names. I didn’t want a wall of red “folder already exists” errors every time.
Here’s a cleaner version that checks first:
$ClientID = "655a839c-8659-AAAAAAAAAAAA"
$SiteUrl = "https://tsinfotechnologies.sharepoint.com/sites/PnPPowerShell"
$LibraryName = "Training Documents"
$Folders = @("HR","Finance","IT","Sales","Marketing","Projects","Legal","Operations")
Connect-PnPOnline -Url $SiteUrl -ClientId $ClientID -Interactive
# Create library if missing
if (-not (Get-PnPList -Identity $LibraryName -ErrorAction SilentlyContinue)) {
New-PnPList -Title $LibraryName -Template DocumentLibrary -OnQuickLaunch | Out-Null
Write-Host "Library created: $LibraryName" -ForegroundColor Green
}
$LibraryUrl = (Get-PnPList -Identity $LibraryName -Includes RootFolder).RootFolder.ServerRelativeUrl
# Get folders that already exist
$ExistingFolders = Get-PnPFolderItem -FolderSiteRelativeUrl $LibraryName -ItemType Folder |
Select-Object -ExpandProperty Name
foreach ($Folder in $Folders) {
if ($ExistingFolders -contains $Folder) {
Write-Host "Skipped (already exists): $Folder" -ForegroundColor DarkGray
}
else {
Add-PnPFolder -Name $Folder -Folder $LibraryUrl | Out-Null
Write-Host "Created: $Folder" -ForegroundColor Green
}
}
Disconnect-PnPOnline
Now when I re-run it, only the new folders (Legal and Operations) get created, and the output is clean and readable.
Method 3: Read Folder Names from a CSV File
This is the method I ended up using for the actual client delivery. Their HR team wanted to control the folder list themselves, and asking them to edit a PowerShell array was never going to happen. A CSV file, on the other hand, is something anyone can open in Excel.
Step 1: Create the CSV file
I created a file called Folders.csv at C:\Scripts\Folders.csv:
FolderName
HR
Finance
IT
Sales
Marketing
Projects
Legal
Operations
Facilities
The first row is the header, and it matters – the script references it by name.
Step 2: The script
$ClientID = "655a839c-8659-AAAAAAAAAAAA"
$SiteUrl = "https://tsinfotechnologies.sharepoint.com/sites/PnPPowerShell"
$LibraryName = "Training Documents"
$CSVPath = "C:\Scripts\Folders.csv"
# Make sure the CSV actually exists before we do anything
if (-not (Test-Path $CSVPath)) {
Write-Host "CSV file not found at $CSVPath" -ForegroundColor Red
return
}
Connect-PnPOnline -Url $SiteUrl -ClientId $ClientID -Interactive
if (-not (Get-PnPList -Identity $LibraryName -ErrorAction SilentlyContinue)) {
New-PnPList -Title $LibraryName -Template DocumentLibrary -OnQuickLaunch | Out-Null
Write-Host "Library created: $LibraryName" -ForegroundColor Green
}
$LibraryUrl = (Get-PnPList -Identity $LibraryName -Includes RootFolder).RootFolder.ServerRelativeUrl
$FolderData = Import-Csv -Path $CSVPath
foreach ($Row in $FolderData) {
$FolderName = $Row.FolderName.Trim()
if ([string]::IsNullOrWhiteSpace($FolderName)) { continue }
try {
Add-PnPFolder -Name $FolderName -Folder $LibraryUrl -ErrorAction Stop | Out-Null
Write-Host "Created: $FolderName" -ForegroundColor Green
}
catch {
Write-Host "Failed: $FolderName - $($_.Exception.Message)" -ForegroundColor Red
}
}
Disconnect-PnPOnline
A couple of things worth calling out here:
.Trim()– When people edit CSVs in Excel, trailing spaces sneak in constantly. SharePoint will either reject the folder name or create a folder with a weird name. Trimming saves you a support ticket.Test-Path– Always validate the file exists before connecting. There’s no point authenticating if the script is going to fail on line 20.continue– Skips blank rows, which Excel loves to leave at the end of files.
Method 4: Create Nested Subfolders (Multi-Level Folder Structure)
My client didn’t just want top-level folders. Inside each department folder, they wanted three subfolders: Policies, Onboarding, and Archive.
Add-PnPFolder can only create one level at a time, so you need to loop through the hierarchy.
Here’s how I handled it:
$ClientID = "655a839c-8659-AAAAAAAAAAAA"
$SiteUrl = "https://tsinfotechnologies.sharepoint.com/sites/PnPPowerShell"
$LibraryName = "Training Documents"
$Departments = @("HR","Finance","IT","Sales","Marketing","Projects")
$SubFolders = @("Policies","Onboarding","Archive")
Connect-PnPOnline -Url $SiteUrl -ClientId $ClientID -Interactive
if (-not (Get-PnPList -Identity $LibraryName -ErrorAction SilentlyContinue)) {
New-PnPList -Title $LibraryName -Template DocumentLibrary -OnQuickLaunch | Out-Null
}
$LibraryUrl = (Get-PnPList -Identity $LibraryName -Includes RootFolder).RootFolder.ServerRelativeUrl
foreach ($Dept in $Departments) {
# Level 1 - department folder
try {
Add-PnPFolder -Name $Dept -Folder $LibraryUrl -ErrorAction Stop | Out-Null
Write-Host "Created: $Dept" -ForegroundColor Green
}
catch {
Write-Host "Exists: $Dept" -ForegroundColor DarkGray
}
# Level 2 - subfolders inside the department folder
foreach ($Sub in $SubFolders) {
$ParentPath = "$LibraryUrl/$Dept"
try {
Add-PnPFolder -Name $Sub -Folder $ParentPath -ErrorAction Stop | Out-Null
Write-Host " Created: $Dept/$Sub" -ForegroundColor Green
}
catch {
Write-Host " Exists: $Dept/$Sub" -ForegroundColor DarkGray
}
}
}
Disconnect-PnPOnline
This creates 6 department folders and 18 subfolders – 24 folders total – in about 15 seconds.
A Shortcut: Use Resolve-PnPFolder for Deep Paths
If you need to create a deep path in one shot, Resolve-PnPFolder is genuinely one of the most underrated cmdlets in the module. It creates every folder in the path that doesn’t already exist, and quietly does nothing for the ones that do.
$Paths = @(
"Training Documents/HR/Policies/2026",
"Training Documents/HR/Onboarding/Templates",
"Training Documents/Finance/Budgets/Q1",
"Training Documents/IT/Security/Incident Reports"
)
foreach ($Path in $Paths) {
Resolve-PnPFolder -SiteRelativePath $Path | Out-Null
Write-Host "Ensured path: $Path" -ForegroundColor Green
}
Four lines of actual logic, and you get a fully built four-level folder tree. This is my go-to whenever the structure is more than two levels deep.
Method 5: Create the Library and Folders Across Multiple Sites
This was the part that saved me the entire afternoon on the client project. Instead of opening the script 40 times and changing the site URL each time, I put all the site URLs into an array and let PowerShell do the walking.
$ClientID = "655a839c-8659-AAAAAAAAAAAA"
$LibraryName = "Training Documents"
$Folders = @("HR","Finance","IT","Sales","Marketing","Projects")
$SiteUrls = @(
"https://tsinfotechnologies.sharepoint.com/sites/PnPPowerShell",
"https://tsinfotechnologies.sharepoint.com/sites/HRPortal",
"https://tsinfotechnologies.sharepoint.com/sites/FinanceHub"
)
foreach ($Site in $SiteUrls) {
Write-Host "`nProcessing site: $Site" -ForegroundColor Cyan
try {
Connect-PnPOnline -Url $Site -ClientId $ClientID -Interactive -ErrorAction Stop
}
catch {
Write-Host " Could not connect to $Site - $($_.Exception.Message)" -ForegroundColor Red
continue
}
# Create the library if it isn't already there
if (-not (Get-PnPList -Identity $LibraryName -ErrorAction SilentlyContinue)) {
New-PnPList -Title $LibraryName -Template DocumentLibrary -OnQuickLaunch | Out-Null
Write-Host " Library created: $LibraryName" -ForegroundColor Green
}
else {
Write-Host " Library already exists: $LibraryName" -ForegroundColor Yellow
}
$LibraryUrl = (Get-PnPList -Identity $LibraryName -Includes RootFolder).RootFolder.ServerRelativeUrl
foreach ($Folder in $Folders) {
try {
Add-PnPFolder -Name $Folder -Folder $LibraryUrl -ErrorAction Stop | Out-Null
Write-Host " Created folder: $Folder" -ForegroundColor Green
}
catch {
Write-Host " Folder exists or failed: $Folder" -ForegroundColor DarkGray
}
}
Disconnect-PnPOnline
}
Write-Host "`nAll sites processed." -ForegroundColor Cyan
The continue statement inside the catch block is the key detail here. If one site is unreachable (maybe it was deleted, or you don’t have permission), the script logs it and moves straight on to the next site instead of stopping the whole run.
Check out Check if SharePoint Site Already Exists in Power Automate
Method 6: Get the Site List Dynamically Instead of Hardcoding It
Hardcoding 40 URLs is fine once, but it goes stale fast. On the client project, new department sites were being created every week.
So instead of maintaining a list, I pulled the sites directly from the tenant:
$ClientID = "655a839c-8659-AAAAAAAAAAAA"
$AdminUrl = "https://tsinfotechnologies-admin.sharepoint.com"
$LibraryName = "Training Documents"
$Folders = @("HR","Finance","IT","Sales","Marketing","Projects")
# Connect to the SharePoint Admin Center first
Connect-PnPOnline -Url $AdminUrl -ClientId $ClientID -Interactive
# Pull every team site, excluding OneDrive personal sites
$Sites = Get-PnPTenantSite -Template "GROUP#0" |
Where-Object { $_.Url -notlike "*-my.sharepoint.com*" }
Write-Host "Found $($Sites.Count) sites to process." -ForegroundColor Cyan
foreach ($Site in $Sites) {
Write-Host "`nProcessing: $($Site.Url)" -ForegroundColor Cyan
try {
Connect-PnPOnline -Url $Site.Url -ClientId $ClientID -Interactive -ErrorAction Stop
if (-not (Get-PnPList -Identity $LibraryName -ErrorAction SilentlyContinue)) {
New-PnPList -Title $LibraryName -Template DocumentLibrary -OnQuickLaunch | Out-Null
}
$LibraryUrl = (Get-PnPList -Identity $LibraryName -Includes RootFolder).RootFolder.ServerRelativeUrl
foreach ($Folder in $Folders) {
Add-PnPFolder -Name $Folder -Folder $LibraryUrl -ErrorAction SilentlyContinue | Out-Null
}
Write-Host " Done: $($Site.Title)" -ForegroundColor Green
}
catch {
Write-Host " Failed: $($Site.Url) - $($_.Exception.Message)" -ForegroundColor Red
}
}
Disconnect-PnPOnline
You’ll need to be a SharePoint Administrator to run Get-PnPTenantSite. If you only want a subset of sites, filter on the URL:
$Sites = Get-PnPTenantSite | Where-Object { $_.Url -like "*/sites/Dept-*" }
That filter alone turned a 40-site guessing game into a precise, repeatable target list.
Method 7: Create the Library with Custom Settings at the Same Time
Creating the library is only half the job. Nine times out of ten, the client also wants versioning turned on, content approval configured, or the “Open in browser” default set.
Rather than going back into the settings page afterwards, I configure it in the same script:
$LibraryName = "Training Documents"
# Create the library
$Library = New-PnPList -Title $LibraryName `
-Template DocumentLibrary `
-OnQuickLaunch `
-EnableVersioning `
-MajorVersions 100
# Apply additional settings
Set-PnPList -Identity $LibraryName `
-Description "Central library for all department training material" `
-EnableVersioning $true `
-MajorVersions 100 `
-EnableMinorVersions $false `
-ForceCheckout $false `
-EnableAttachments $false
Write-Host "Library configured: $LibraryName" -ForegroundColor Green
Here’s what each of those settings actually does in practice:
| Parameter | What it does |
|---|---|
-EnableVersioning | Keeps a history of every file change |
-MajorVersions 100 | Retains the last 100 versions of each file |
-EnableMinorVersions | Turns draft (0.1, 0.2) versions on or off |
-ForceCheckout | Requires users to check out a file before editing |
-OnQuickLaunch | Shows the library in the left navigation |
-Description | Sets the library description users see |
I always turn versioning on for document libraries. The one time I didn’t, a user overwrote a 60-page policy document, and there was no way to recover it. Never again.
Add Custom Columns to the Document Library
While I’m at it, I usually add the metadata columns the client asked for:
powershellCopy
Add-PnPField -List $LibraryName `
-DisplayName "Department" `
-InternalName "Department" `
-Type Choice `
-Choices "HR","Finance","IT","Sales","Marketing","Projects" `
-AddToDefaultView
Add-PnPField -List $LibraryName `
-DisplayName "Review Date" `
-InternalName "ReviewDate" `
-Type DateTime `
-AddToDefaultView
Add-PnPField -List $LibraryName `
-DisplayName "Document Owner" `
-InternalName "DocumentOwner" `
-Type User `
-AddToDefaultView
The -AddToDefaultView switch is the one people forget. Without it, the column gets created but nobody can see it, and you’ll get an email asking why the script “didn’t work.”
Method 8: Create Folders from a CSV That Includes Nested Paths
This is the most flexible approach of the lot, and it’s what I’d recommend if your folder structure is anything other than a simple flat list.
Instead of one column of folder names, the CSV holds full relative paths.
The CSV file (FolderStructure.csv)
FolderPath
Training Documents/HR
Training Documents/HR/Policies
Training Documents/HR/Onboarding
Training Documents/Finance
Training Documents/Finance/Budgets
Training Documents/Finance/Invoices
Training Documents/IT
Training Documents/IT/Security
Training Documents/IT/Security/Incident Reports
Training Documents/Projects/2026/Q1
The script
$ClientID = "655a839c-8659-AAAAAAAAAAAA"
$SiteUrl = "https://tsinfotechnologies.sharepoint.com/sites/PnPPowerShell"
$LibraryName = "Training Documents"
$CSVPath = "C:\Scripts\FolderStructure.csv"
if (-not (Test-Path $CSVPath)) {
Write-Host "CSV not found: $CSVPath" -ForegroundColor Red
return
}
Connect-PnPOnline -Url $SiteUrl -ClientId $ClientID -Interactive
if (-not (Get-PnPList -Identity $LibraryName -ErrorAction SilentlyContinue)) {
New-PnPList -Title $LibraryName -Template DocumentLibrary -OnQuickLaunch | Out-Null
Write-Host "Library created: $LibraryName" -ForegroundColor Green
}
$Paths = Import-Csv -Path $CSVPath
foreach ($Row in $Paths) {
$Path = $Row.FolderPath.Trim()
if ([string]::IsNullOrWhiteSpace($Path)) { continue }
try {
Resolve-PnPFolder -SiteRelativePath $Path -ErrorAction Stop | Out-Null
Write-Host "Ensured: $Path" -ForegroundColor Green
}
catch {
Write-Host "Failed: $Path - $($_.Exception.Message)" -ForegroundColor Red
}
}
Disconnect-PnPOnline
Because Resolve-PnPFolder creates every missing level in the path, you don’t even need to list the parent folders first. That last row in my CSV – Training Documents/Projects/2026/Q1 – creates Projects, then 2026, then Q1, all in one call.
This is the script I handed over to the client. They edit the CSV in Excel, run one command, and the structure updates. No PowerShell knowledge required on their end.
Check out Build a SharePoint Folder Tree View Using SharePoint Framework (SPFx)
Method 9: Add a Logging and Reporting Layer
When you’re running a script across dozens of SharePoint sites, “it seemed to work” isn’t good enough. I always output a CSV report, so I have proof of what happened.
$ClientID = "655a839c-8659-AAAAAAAAAAAA"
$LibraryName = "Training Documents"
$Folders = @("HR","Finance","IT","Sales","Marketing","Projects")
$ReportPath = "C:\Scripts\FolderCreationReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv"
$SiteUrls = @(
"https://tsinfotechnologies.sharepoint.com/sites/PnPPowerShell",
"https://tsinfotechnologies.sharepoint.com/sites/HRPortal"
)
$Report = @()
foreach ($Site in $SiteUrls) {
try {
Connect-PnPOnline -Url $Site -ClientId $ClientID -Interactive -ErrorAction Stop
if (-not (Get-PnPList -Identity $LibraryName -ErrorAction SilentlyContinue)) {
New-PnPList -Title $LibraryName -Template DocumentLibrary -OnQuickLaunch | Out-Null
}
$LibraryUrl = (Get-PnPList -Identity $LibraryName -Includes RootFolder).RootFolder.ServerRelativeUrl
foreach ($Folder in $Folders) {
$Status = "Created"
$Message = ""
try {
Add-PnPFolder -Name $Folder -Folder $LibraryUrl -ErrorAction Stop | Out-Null
}
catch {
$Status = "Failed"
$Message = $_.Exception.Message
}
$Report += [PSCustomObject]@{
SiteUrl = $Site
Library = $LibraryName
FolderName = $Folder
Status = $Status
Message = $Message
TimeStamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
}
}
}
catch {
$Report += [PSCustomObject]@{
SiteUrl = $Site
Library = $LibraryName
FolderName = "N/A"
Status = "Connection Failed"
Message = $_.Exception.Message
TimeStamp = (Get-Date -Format "yyyy-MM-dd HH:mm:ss")
}
}
}
$Report | Export-Csv -Path $ReportPath -NoTypeInformation
Write-Host "`nReport saved to: $ReportPath" -ForegroundColor Cyan
Disconnect-PnPOnline
The timestamped filename means every run gets its own report file, so you build up a history rather than overwriting the last one.
How to Verify the Folders Were Actually Created
Never assume the script worked just because there were no red errors. Here’s the quickest way to check:
Connect-PnPOnline -Url $SiteUrl -ClientId $ClientID -Interactive
# List every folder in the library
Get-PnPFolderItem -FolderSiteRelativeUrl "Training Documents" -ItemType Folder |
Select-Object Name, ServerRelativeUrl |
Format-Table -AutoSize
To see the full recursive structure including subfolders:
Get-PnPFolderItem -FolderSiteRelativeUrl "Training Documents" -ItemType Folder -Recursive |
Select-Object Name, ServerRelativeUrl |
Sort-Object ServerRelativeUrl |
Format-Table -AutoSize
And to get a simple count:
$Count = (Get-PnPFolderItem -FolderSiteRelativeUrl "Training Documents" -ItemType Folder -Recursive).Count
Write-Host "Total folders created: $Count" -ForegroundColor Green
I run this verification step every single time before telling a client the job is done.
Read Create a Folder in SharePoint From Power Apps
Common Errors and How I Fixed Them
These are the errors I hit on the project, and exactly what solved each one.
“The remote server returned an error: (401) Unauthorized”
You’re either not connected, or your connection expired. Reconnect:
Connect-PnPOnline -Url $SiteUrl -ClientId $ClientID -Interactive
If it keeps happening on a long-running script, add a reconnect inside the loop rather than relying on one connection for an hour.
“Add-PnPFolder: File Not Found”
This is the one from the original script. The -Folder parameter is receiving a path that doesn’t exist. Always pull the real URL from the list object:
$LibraryUrl = (Get-PnPList -Identity "Training Documents" -Includes RootFolder).RootFolder.ServerRelativeUrl
Add-PnPFolder -Name "HR" -Folder $LibraryUrl
A list, survey, discussion board, or document library with the specified title already exists in this website”
This is what you get when you re-run New-PnPList against a site that already has the library. The fix is the existence check I showed earlier:
if (-not (Get-PnPList -Identity $LibraryName -ErrorAction SilentlyContinue)) {
New-PnPList -Title $LibraryName -Template DocumentLibrary -OnQuickLaunch | Out-Null
}
else {
Write-Host "Library already exists. Skipping." -ForegroundColor Yellow
}
I now put this check in every provisioning script by default. It costs one extra line and makes the script safely re-runnable, which matters a lot when you’re processing 40 sites and one of them fails halfway through.
“The file or folder name contains invalid characters”
SharePoint blocks these characters in folder names:
" * : < > ? / \ |
It also rejects names that start or end with a period, and the reserved name forms. If your folder names are coming from a CSV that someone else maintains, sanitize them before you use them:
function Get-CleanFolderName {
param([string]$Name)
$Invalid = '["*:<>?/\\|]'
$Clean = $Name -replace $Invalid, ''
$Clean = $Clean.Trim().Trim('.')
return $Clean
}
foreach ($Row in $FolderData) {
$FolderName = Get-CleanFolderName -Name $Row.FolderName
if ([string]::IsNullOrWhiteSpace($FolderName)) { continue }
if ($FolderName -eq "forms") { continue }
Add-PnPFolder -Name $FolderName -Folder $LibraryUrl -ErrorAction SilentlyContinue | Out-Null
}
I added this after a client CSV came through with a folder called Q1/Q2 Reports. That forward slash silently broke the run, and I spent twenty minutes working out why only half the folders appeared.
Frequently Asked Questions
Can I create a document library without a Client ID?
No, not anymore. Since PnP PowerShell removed its built-in multi-tenant app, you have to supply your own -ClientId. Run Register-PnPEntraIDAppForInteractiveLogin once and reuse the GUID it gives you.
What’s the difference between New-PnPList and New-PnPDocumentLibrary?
New-PnPList with -Template DocumentLibrary is the approach I use because it exposes more parameters in one call. Both create a document library; New-PnPList just gives you more control.
How do I delete a folder I created by mistake?
Remove-PnPFolder -Name “Marketing” -Folder “Training Documents” -Force
How do I rename an existing folder?
$Folder = Get-PnPFolder -Url “Training Documents/Sales”
Rename-PnPFolder -Folder $Folder -TargetFolderName “Sales and Revenue”
Can I set unique permissions on each folder?
Yes. Break inheritance first, then assign:$Item = Get-PnPFolder -Url "Training Documents/HR" -Includes ListItemAllFields Set-PnPListItemPermission -List "Training Documents" ` -Identity $Item.ListItemAllFields.Id ` -Group "HR Team" ` -AddRole "Contribute" ` -ClearExisting
Be careful with this one. Unique permissions on hundreds of folders becomes a maintenance headache very quickly. I only do it where there’s a genuine compliance reason.
How many folders can a document library hold?
A library can hold up to 30 million items. The practical limit you’ll hit first is the 5,000-item view threshold — once a single folder holds more than 5,000 items, views start failing unless you have indexed columns and filtered views in place.
Does this work for SharePoint on-premises?
The modern PnP.PowerShell module targets SharePoint Online only. For on-premises you’d need the legacy SharePointPnPPowerShell modules, which use different authentication.
Conclusion
What started as a fairly boring client request — “can you set up a Training Documents library with department folders on all our sites?” — turned into a script I now reuse on almost every project.
To recap what I covered:
- Method 1 fixed the three issues in the common starter script: missing
-Interactive, using the library title instead of its real URL, and no existence check. - Method 2 made the script safely re-runnable by skipping folders that already exist.
- Method 3 moved the folder list into a CSV so non-technical people can maintain it.
- Method 4 built nested subfolders, and introduced
Resolve-PnPFolderfor deep paths. - Method 5 and 6 scaled it across many sites, hardcoded or pulled dynamically from the tenant.
- Method 7 configured versioning, descriptions, and custom columns during creation.
- Method 8 combined CSV input with nested paths — the version I’d actually recommend.
- Method 9 added logging so you have proof of what ran.
If you only take one thing from this article, make it Resolve-PnPFolder. It creates every missing level of a path in a single call and does nothing when the folder already exists. It replaced about thirty lines of my old looping code with one.
Start with the simple version, test it on a single site, then layer on the CSV input and reporting once you’re confident. That’s the exact order I built it in, and it’s the order that will save you the most time.
You may also like:
- Get SharePoint Folder Permissions Using PowerShell
- Enable Sensitivity Labels For Microsoft 365 Groups & SharePoint Sites Using PowerShell
- Add SharePoint List Fields From Excel Using PnP PowerShell

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.