Why I built a prompt library on our intranet

Why I built a prompt library on our intranet

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:

TitleText (built-in)The short, recognisable name of the prompt — what people scan for.
PromptMultiple lines of textThe full prompt body. This is the thing people actually copy.
SummaryMultiple lines of textA one-line description shown on the card. Tells you what the prompt is for without needing to read it.
CategoryChoiceBroad area: HR, Operations, PM, Legal, Comms, Marketing, Sales, General. Drives the category pill on the card and one of the search refiners.
TagsTextSemicolon-separated keywords (e.g. draft;review;email). Stored as plain text on purpose — see below.
KeywordsTextExtra 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:

  1. Connects to the site you pass in.
  2. Creates the Prompt Library list if it doesn’t exist (skips if it does).
  3. 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.
  4. Marks Prompt, Summary, and Category as required.
  5. Calls Request-PnPReIndexList so 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 propertyManaged property
ows_PromptRefinableString200
ows_SummaryRefinableString201
ows_CategoryRefinableString202
ows_TagsRefinableString203
ows_KeywordsRefinableString204

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:

TitleCategoryWhat it does
Meeting summariserHRTurns a Teams transcript into minutes with decisions and an action table.
Weekly status draftPMWrites a weekly status report with RAG risks from raw notes.
Bid response helperSalesDrafts an RFP answer grounded in your case studies, won’t invent metrics.
Policy Q&ALegalAnswers questions using only your attached policies, cites the section.
Email rewriteCommsRewrites a long email at half the length, preserves the sender’s voice.
Job description writerHRGenerates an inclusive JD from a hiring manager’s notes.
Customer replyOperationsDrafts a calm, accountable reply to an unhappy customer email.
Marketing copy variantsMarketingProduces three on-brand variants for A/B testing, each from a different angle.
Translation with contextCommsTranslates while preserving tone and flagging reviewer-check items.
Brainstorm partnerGeneralPressure-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:

  1. Run Create-PromptLibrary.ps1 — list and columns are provisioned in under a minute.
  2. Run Seed-PromptLibrary.ps1 — ten prompts land in the list immediately.
  3. Wait for a crawl — usually 15-60 minutes.
  4. Map the crawled properties to RefinableString200–204 in the search schema.
  5. Wait for another crawl — same window.
  6. 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 columns
  • Seed-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