Earlier this year I noticed something quietly going wrong at a customer: everyone was using AI, but in completely different ways. Our HR contact got one kind of output. Our project lead got another. Marketing, something else entirely. Same tools. Same models. Wildly different results.
The difference wasn’t the AI. It was the prompts.
Some people had quietly developed a small private collection that worked for them. Others were typing one-liners into Copilot and accepting whatever came back. Nobody was sharing. Nobody was iterating. We were spending the same five minutes badly, ten times a week, across the team.
So I built a prompt library on our intranet. Not a wiki. Not a Notion page. A proper, searchable, filterable library on SharePoint, where every prompt is a list item with a category, tags, a description, and the full prompt text front and centre. People can copy a prompt with a click and send feedback to whoever wrote it through a Teams chat.
This post walks through the technical setup: the list schema, the PowerShell to create it, and a second script that seeds it with ten ready-to-use example prompts.

The front-end is a PnP Modern Search results web part with a custom layout. I won’t go into the template here — it’s a separate topic — but you can see what the end result looks like above. The interesting part for this post is what sits underneath: the list, the columns, and how the data gets in there.
The list schema
The list is a standard SharePoint custom list called Prompt Library, with these columns:
| Title | Text (built-in) | The short, recognisable name of the prompt — what people scan for. |
| Prompt | Multiple lines of text | The full prompt body. This is the thing people actually copy. |
| Summary | Multiple lines of text | A one-line description shown on the card. Tells you what the prompt is for without needing to read it. |
| Category | Choice | Broad area: HR, Operations, PM, Legal, Comms, Marketing, Sales, General. Drives the category pill on the card and one of the search refiners. |
| Tags | Text | Semicolon-separated keywords (e.g. draft;review;email). Stored as plain text on purpose — see below. |
| Keywords | Text | Extra search terms that don’t appear on the card but help things turn up in search. Stuff like alternate spellings, synonyms, related concepts. |
A note on Tags: it’s tempting to use a Managed Metadata column for this, and in a more mature setup that’s the right call. For a first version I deliberately kept it as plain text with a ; separator. It’s faster to provision, easier to seed from a script, and the front-end just splits on the semicolon to render chips. When the library is bigger and people are arguing about whether email and mail should be the same tag, that’s the moment to graduate to managed metadata.
A note on Keywords: this column is the secret weapon for discoverability. If you write a prompt called “Customer reply (de-escalating)” but everyone in your company calls it a “complaint reply”, they’ll never find it by searching for the title. Stuffing the keyword field with all the natural ways someone might phrase the request fixes that without cluttering the visible card.
Script 1 — create the list
Rather than clicking through the SharePoint UI to add columns one by one, I wrote a PnP PowerShell script. It’s idempotent (safe to re-run — it skips fields that already exist), so you can use it both for first-time provisioning and for adding columns later.
powershell
.\Create-PromptLibrary.ps1 -SiteUrl "https://tenant.sharepoint.com/sites/Intranet"
Here’s what it does, in order:
- Connects to the site you pass in.
- Creates the Prompt Library list if it doesn’t exist (skips if it does).
- Adds each column with the correct type — Note for the multi-line ones, Choice for Category with the eight categories pre-baked, plain Text for Tags and Keywords.
- Marks Prompt, Summary, and Category as required.
- Calls
Request-PnPReIndexListso the new columns get picked up by the next search crawl.
The full script is in the appendix at the bottom of this post.
The reindex step matters more than it sounds. Without it, you’ll add data and then sit there confused for a few hours wondering why nothing shows up in your search results. The reindex flag tells SharePoint “please look at this list again next time you crawl”, which on a normal tenant means within an hour or so.
Search schema mapping (the bit you can’t script)
Once the list exists and has been crawled, you need to map the crawled properties to managed properties. This is a manual step in the SharePoint Admin Centre → Search → Search Schema, and it can’t be automated through PnP without admin-level Search Service access:
| Crawled property | → | Managed property |
|---|---|---|
ows_Prompt | → | RefinableString200 |
ows_Summary | → | RefinableString201 |
ows_Category | → | RefinableString202 |
ows_Tags | → | RefinableString203 |
ows_Keywords | → | RefinableString204 |
Set each one to Retrievable so the search results web part can pull the value, and Refinable if you want to use it in a filter.
This is the slowest part of the whole setup — not because the mapping itself is hard, but because each change needs another full crawl before your data shows up. Plan for it across a couple of work sessions, not a single afternoon.
Script 2 — seed the test data
Once the list exists, I wanted realistic data in it from day one. Empty libraries don’t get used. So I wrote a second script that drops in ten genuinely useful prompts covering the eight categories:
powershell
.\Seed-PromptLibrary.ps1 -SiteUrl "https://tenant.sharepoint.com/sites/Intranet"
The script holds the prompts in a PowerShell array of hashtables and loops through them, calling Add-PnPListItem for each one. Using a here-string (@" ... "@) for the Prompt field preserves the line breaks and indentation, which is crucial — a prompt’s structure is part of how it works, and flattening it to one line genuinely changes the output you get from an LLM.
The ten prompts are not placeholder lorem-ipsum — they’re real, working prompts I use myself. If you run the script you get a working starting library straight away:
| Title | Category | What it does |
|---|---|---|
| Meeting summariser | HR | Turns a Teams transcript into minutes with decisions and an action table. |
| Weekly status draft | PM | Writes a weekly status report with RAG risks from raw notes. |
| Bid response helper | Sales | Drafts an RFP answer grounded in your case studies, won’t invent metrics. |
| Policy Q&A | Legal | Answers questions using only your attached policies, cites the section. |
| Email rewrite | Comms | Rewrites a long email at half the length, preserves the sender’s voice. |
| Job description writer | HR | Generates an inclusive JD from a hiring manager’s notes. |
| Customer reply | Operations | Drafts a calm, accountable reply to an unhappy customer email. |
| Marketing copy variants | Marketing | Produces three on-brand variants for A/B testing, each from a different angle. |
| Translation with context | Comms | Translates while preserving tone and flagging reviewer-check items. |
| Brainstorm partner | General | Pressure-tests an idea with structured, contrarian feedback. |
The full script with all ten prompts is in the appendix.
Putting it together
The end-to-end setup, start to finish, looks like this:
- Run
Create-PromptLibrary.ps1— list and columns are provisioned in under a minute. - Run
Seed-PromptLibrary.ps1— ten prompts land in the list immediately. - Wait for a crawl — usually 15-60 minutes.
- Map the crawled properties to RefinableString200–204 in the search schema.
- Wait for another crawl — same window.
- Add a PnP Modern Search results web part to a page, point it at the list with a query like
Path:"https://yourtenant.sharepoint.com/sites/Intranet/Lists/Prompt Library" ContentTypeId:0x0100*, and add the RefinableStringXXX values to the Selected Properties list.
That’s the entire backend. The custom Handlebars template that turns the search results into the cards you see in the screenshot is a story for another post.
What I’d do differently
Two things, in hindsight:
Tags should be Managed Metadata from day one if you have any plan to grow this beyond a handful of users. The plain-text approach works, but tag drift sets in fast — email, e-mail, mail, Email will all turn up as separate “tags” within a week.
Categories should probably be a managed term set too for the same reason, plus it gives you better refiner behaviour in search. Choice columns are easier to script but harder to govern.
For a v1 to prove the idea, plain text and choice columns are completely fine. They’re what I started with, and they got the library in front of users a week sooner than the “proper” version would have.
Appendix — the scripts
I’ve published both scripts as standalone files. Drop them in the same folder, install the PnP PowerShell module if you don’t have it (Install-Module PnP.PowerShell -Scope CurrentUser), and run them in order:
Create-PromptLibrary.ps1— provisions the list and columnsSeed-PromptLibrary.ps1— fills it with the ten example prompts
If you want to see the front-end too — the search-results template, the card layout, the Teams feedback button — get in touch. Happy to walk you through it.
Create-PromptLibrary.ps1
<#
.SYNOPSIS
Creates the Prompt Library SharePoint list with all required columns.
.DESCRIPTION
Provisions a custom list called "Prompt Library" on a SharePoint Online
site, adds the columns needed for a searchable prompt library
(Title, Prompt, Summary, Category, Tags, Keywords), and configures the
default view.
.PARAMETER SiteUrl
The full URL of the SharePoint site where the list will be created.
.EXAMPLE
.\Create-PromptLibrary.ps1 -SiteUrl "https://tenant.sharepoint.com/sites/Intranet"
.NOTES
Requires PnP.PowerShell module. Install with:
Install-Module PnP.PowerShell -Scope CurrentUser
#>
param(
[Parameter(Mandatory = $true)]
[string]$SiteUrl
)
# ---------------------------------------------------------------------------
# Connect
# ---------------------------------------------------------------------------
Write-Host "Connecting to $SiteUrl ..." -ForegroundColor Cyan
Connect-PnPOnline -Url $SiteUrl -ClientId "GUID" -Interactive
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
$listTitle = "Prompt Library"
$listDescription = "A curated library of AI prompts for the team."
$choicesCategory = @(
"HR", "Operations", "PM", "Legal", "Comms",
"Marketing", "Sales", "General"
)
# ---------------------------------------------------------------------------
# Create the list (or skip if it already exists)
# ---------------------------------------------------------------------------
$existing = Get-PnPList -Identity $listTitle -ErrorAction SilentlyContinue
if ($existing) {
Write-Host "List '$listTitle' already exists — skipping creation." -ForegroundColor Yellow
} else {
Write-Host "Creating list '$listTitle' ..." -ForegroundColor Cyan
New-PnPList -Title $listTitle -Template GenericList -OnQuickLaunch | Out-Null
Set-PnPList -Identity $listTitle -Description $listDescription
}
# ---------------------------------------------------------------------------
# Add columns
# ---------------------------------------------------------------------------
# Helper: add a field only if it doesn't exist yet
function Add-FieldIfMissing {
param(
[string]$List,
[string]$InternalName,
[string]$DisplayName,
[string]$Type,
[bool] $Required = $false,
[string[]]$Choices = $null
)
$field = Get-PnPField -List $List -Identity $InternalName -ErrorAction SilentlyContinue
if ($field) {
Write-Host " • Field '$DisplayName' exists — skipping." -ForegroundColor DarkGray
return
}
Write-Host " • Adding field '$DisplayName' ($Type) ..." -ForegroundColor Cyan
if ($Type -eq "Choice" -and $Choices) {
Add-PnPField -List $List `
-DisplayName $DisplayName `
-InternalName $InternalName `
-Type Choice `
-Choices $Choices `
-AddToDefaultView | Out-Null
} else {
Add-PnPField -List $List `
-DisplayName $DisplayName `
-InternalName $InternalName `
-Type $Type `
-AddToDefaultView | Out-Null
}
if ($Required) {
Set-PnPField -List $List -Identity $InternalName -Values @{ Required = $true }
}
}
# Prompt — the full prompt text (multiline, plain text)
Add-FieldIfMissing -List $listTitle -InternalName "Prompt" `
-DisplayName "Prompt" -Type "Note" -Required $true
# Summary — short description shown on the card
Add-FieldIfMissing -List $listTitle -InternalName "Summary" `
-DisplayName "Summary" -Type "Note" -Required $true
# Category — the broad area the prompt belongs to
Add-FieldIfMissing -List $listTitle -InternalName "Category" `
-DisplayName "Category" -Type "Choice" -Required $true `
-Choices $choicesCategory
# Tags — semicolon-separated keywords (kept as plain text on purpose)
Add-FieldIfMissing -List $listTitle -InternalName "Tags" `
-DisplayName "Tags" -Type "Text"
# Keywords — extra terms to match in search but not shown
Add-FieldIfMissing -List $listTitle -InternalName "Keywords" `
-DisplayName "Keywords" -Type "Text"
# ---------------------------------------------------------------------------
# Trigger a reindex so the new columns are picked up by search quickly
# ---------------------------------------------------------------------------
Write-Host "Requesting list reindex ..." -ForegroundColor Cyan
Request-PnPReIndexList -Identity $listTitle
Write-Host ""
Write-Host "Done. List '$listTitle' is ready." -ForegroundColor Green
Write-Host "Next steps:" -ForegroundColor Green
Write-Host " 1. Wait for the next search crawl (or force one in the SharePoint admin centre)."
Write-Host " 2. Map the crawled properties (ows_Prompt, ows_Summary, ows_Category, ows_Tags)"
Write-Host " to managed properties (e.g. RefinableString200..203)."
Write-Host " 3. Run .\Seed-PromptLibrary.ps1 to populate the list with example prompts."
Seed-PromptLibrary.ps1
<#
.SYNOPSIS
Seeds the Prompt Library list with 10 ready-to-use example prompts.
.PARAMETER SiteUrl
The full URL of the SharePoint site that contains the Prompt Library list.
.EXAMPLE
.\Seed-PromptLibrary.ps1 -SiteUrl "https://tenant.sharepoint.com/sites/Intranet"
.NOTES
Requires PnP.PowerShell. Run Create-PromptLibrary.ps1 first.
#>
param(
[Parameter(Mandatory = $true)]
[string]$SiteUrl
)
Write-Host "Connecting to $SiteUrl ..." -ForegroundColor Cyan
Connect-PnPOnline -Url $SiteUrl -ClientId "GUID" -Interactive
$listTitle = "Prompt Library"
# ---------------------------------------------------------------------------
# The prompts
# ---------------------------------------------------------------------------
$prompts = @(
@{
Title = "Meeting summariser"
Summary = "Turn a Teams transcript into concise minutes with decisions and actions."
Category = "HR"
Tags = "meeting;summary;minutes"
Keywords = "teams transcript minutes actions decisions notes"
Prompt = @"
You are an executive assistant. Summarise the transcript below into:
1. Three to five bullet highlights
2. Decisions made
3. Action items as a table with Owner | Action | Due date
Tone: neutral, factual. Maximum 250 words.
Transcript:
{transcript}
"@
},
@{
Title = "Weekly status draft"
Summary = "Draft a weekly project status update from raw notes."
Category = "PM"
Tags = "draft;report;planning"
Keywords = "status weekly update project rag risks"
Prompt = @"
Act as a project manager. Draft a weekly status update with these sections:
- Progress this week
- Risks (with RAG status: red/amber/green)
- Blockers
- Next week's focus
Tone: confident, specific, no filler. Avoid hedging language.
Raw notes:
{notes}
"@
},
@{
Title = "Bid response helper"
Summary = "Generate a first-pass answer to an RFP question grounded in our case studies."
Category = "Sales"
Tags = "proposal;draft;research"
Keywords = "rfp tender bid proposal procurement"
Prompt = @"
You are a bid manager writing for a procurement audience. Using only the attached case studies and capability statements, draft a 150-word answer to the RFP question below.
Structure your answer as:
- Approach (how we do it)
- Evidence (which case study proves it)
- Outcome (the measurable result)
Do not invent facts or metrics. If the case studies don't cover something, flag it.
RFP question:
{question}
"@
},
@{
Title = "Policy Q&A"
Summary = "Answer an employee question using only our internal policy documents."
Category = "Legal"
Tags = "policy;review"
Keywords = "handbook policy compliance hr legal"
Prompt = @"
You are a policy assistant. Answer the question below using ONLY the referenced policy documents.
Rules:
- If the answer is in the policies, give it plainly and cite the document name and section number.
- If the answer is partial, say what is and isn't covered.
- If the policies don't cover it, say so and point to the policy owner listed in the document.
- Never guess. Never reference external sources.
Question:
{question}
"@
},
@{
Title = "Email rewrite (clear & concise)"
Summary = "Rewrite a long or rambling email into something short, clear and respectful."
Category = "Comms"
Tags = "email;draft;review"
Keywords = "email rewrite tone clarity concise"
Prompt = @"
Rewrite the email below so it is:
- Half the length or shorter
- Clearly structured (one ask per paragraph)
- Polite but direct
- Free of corporate filler ("just circling back", "as per my last email", etc.)
Keep all factual details and dates intact. Preserve the original sender's voice — don't make it sound like a different person.
Original:
{email}
"@
},
@{
Title = "Job description writer"
Summary = "Generate an inclusive, accurate job description from a hiring manager's notes."
Category = "HR"
Tags = "draft;review"
Keywords = "job description hiring recruitment vacancy posting"
Prompt = @"
You are an HR business partner. Using the hiring manager's notes below, write a job description with these sections:
- Role purpose (2 sentences)
- Key responsibilities (5-7 bullets)
- Must-have skills (4-6 bullets)
- Nice-to-have skills (3-4 bullets)
- What we offer (3 bullets)
Constraints:
- Use gender-neutral, inclusive language.
- Avoid jargon and "rockstar/ninja" cliches.
- Don't list a degree requirement unless the manager explicitly asks for one.
- Reading age: secondary school level.
Notes from hiring manager:
{notes}
"@
},
@{
Title = "Customer reply (de-escalating)"
Summary = "Draft a calm, accountable reply to an unhappy customer email."
Category = "Operations"
Tags = "email;draft;review"
Keywords = "customer service complaint apology de-escalation reply"
Prompt = @"
You are a customer service lead. Draft a reply to the customer message below that:
- Acknowledges the issue specifically (not "we hear your concerns")
- Takes accountability where appropriate
- States exactly what we will do and by when
- Avoids defensive or legalistic language
- Ends with a single, clear next step
Tone: warm, professional, human. Maximum 180 words.
Customer message:
{message}
"@
},
@{
Title = "Marketing copy variants"
Summary = "Generate three on-brand variants of marketing copy for A/B testing."
Category = "Marketing"
Tags = "draft;brainstorm"
Keywords = "marketing copy ad campaign variant ab test"
Prompt = @"
You are a senior copywriter. Produce three distinct variants of the copy below for A/B testing.
Each variant should:
- Lead with a different angle (e.g., outcome-led, problem-led, story-led)
- Stay within the same word count as the original (+/- 10%)
- Use our tone of voice: confident, plain-spoken, never salesy
- Avoid superlatives like "best", "leading", "world-class"
Label each variant with the angle it takes, then give the copy.
Original:
{copy}
"@
},
@{
Title = "Translation with context"
Summary = "Translate text between languages while preserving tone and business context."
Category = "Comms"
Tags = "translation;review"
Keywords = "translation localisation language multilingual"
Prompt = @"
Translate the text below from {source_language} to {target_language}.
Rules:
- Preserve tone (formal/informal/marketing/legal — match what the original is doing).
- Keep names, product names and acronyms in their original form.
- For idioms, translate the meaning, not the words.
- If a term has no good equivalent, keep the original and add a short parenthetical explanation.
After the translation, list any choices a human reviewer should sanity-check (max 3 bullets).
Text:
{text}
"@
},
@{
Title = "Brainstorm partner"
Summary = "Pressure-test an idea with structured, contrarian brainstorming."
Category = "General"
Tags = "brainstorm;planning;analysis"
Keywords = "brainstorm strategy ideation challenge devil advocate"
Prompt = @"
You are a strategic sparring partner — sharp, supportive, never sycophantic.
For the idea below, do all of the following:
1. State the idea back to me in one sentence to confirm understanding.
2. List three reasons it might work.
3. List three reasons it might fail (be specific - "the market" is not a reason).
4. Name the single biggest assumption I'm making and how I could test it cheaply.
5. Suggest two adjacent ideas worth exploring.
Be direct. Don't open with compliments.
My idea:
{idea}
"@
}
)
# ---------------------------------------------------------------------------
# Insert
# ---------------------------------------------------------------------------
Write-Host "Adding $($prompts.Count) prompts to '$listTitle' ..." -ForegroundColor Cyan
foreach ($p in $prompts) {
Write-Host " • $($p.Title)" -ForegroundColor Gray
Add-PnPListItem -List $listTitle -Values @{
Title = $p.Title
Prompt = $p.Prompt
Summary = $p.Summary
Category = $p.Category
Tags = $p.Tags
Keywords = $p.Keywords
} | Out-Null
}
Write-Host ""
Write-Host "Done. $($prompts.Count) prompts added." -ForegroundColor Green