How to Create Document Library in SharePoint Using PnP PowerShell (With Multiple Folders)

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.

Table of Contents:

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:

PowerShell Create a Document Library and Multiple Folders

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: PoliciesOnboarding, 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:

ParameterWhat it does
-EnableVersioningKeeps a history of every file change
-MajorVersions 100Retains the last 100 versions of each file
-EnableMinorVersionsTurns draft (0.1, 0.2) versions on or off
-ForceCheckoutRequires users to check out a file before editing
-OnQuickLaunchShows the library in the left navigation
-DescriptionSets 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-PnPFolder for 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:

Power Apps functions free pdf

30 Power Apps Functions

This free guide walks you through the 30 most-used Power Apps functions with real business examples, exact syntax, and results you can see.

Live Webinar

SharePoint Integration Power Apps Form With Repeating Table [Invoice Management System]

Learn how to build an invoice management system using SharePoint integration and a repeating table.

📅 2nd September 2026 – 10:00 AM EST | 7:30 PM IST

Download User registration canvas app

DOWNLOAD USER REGISTRATION POWER APPS CANVAS APP

Download a fully functional Power Apps Canvas App (with Power Automate): User Registration App