Build a prompt-template library in Sheets
Manage and reuse Northwind prompts systematically — one source of truth, every AI script reads from it.
Published Dec 9, 2025
Once Northwind had more than a couple of AI scripts running, the prompts were a problem. The same instructions were pasted into half a dozen files, each slightly different, and tweaking the tone of a customer reply meant hunting through every script to find the wording.
This automation makes the spreadsheet the single source of truth. A Prompts
sheet holds every prompt template by name; a small library reads it, caches it,
and lets any script call a prompt by name with variables filled in. Edit a
prompt in the sheet and every script picks up the change — no code edits, no
copies drifting out of sync.
What you’ll need
- A Google Sheet with a
Promptstab. Three columns with a header row:name(the key scripts call),template(the prompt text, with{{placeholders}}for variables), andmodel(optional — the Claude model to use, with a default applied if blank). - The sheet’s ID for the
PROMPTS_SHEETconstant. - An Anthropic API key saved as
ANTHROPIC_API_KEYin Script Properties — see Store API keys and secrets securely.
The library
// The spreadsheet whose "Prompts" tab holds every template.
const PROMPTS_SHEET = '1abcPromptsId';
// Used when a prompt row leaves the "model" column blank.
const DEFAULT_MODEL = 'claude-haiku-4-5-20251001';
// How long (seconds) to cache the loaded prompts before re-reading.
const CACHE_TTL = 3600;
/**
* Loads every prompt from the sheet into a name-keyed object.
* Caches the result so repeated calls do not re-read the sheet.
* @return {Object} Map of prompt name to { template, model }.
*/
function loadPrompts() {
// 1. Return the cached copy if it is still warm.
const cache = CacheService.getScriptCache();
const hit = cache.get('prompts');
if (hit) return JSON.parse(hit);
// 2. Cache miss — read the Prompts sheet, dropping the header row.
const [, ...rows] = SpreadsheetApp.openById(PROMPTS_SHEET)
.getSheets()[0]
.getDataRange()
.getValues();
// 3. Build a { name: { template, model } } map.
const out = Object.fromEntries(rows
.filter((r) => r[0]) // Skip any blank rows.
.map((r) => [r[0], { template: r[1], model: r[2] || DEFAULT_MODEL }]));
// 4. Cache it and return.
cache.put('prompts', JSON.stringify(out), CACHE_TTL);
return out;
}
/**
* Looks up a prompt by name, substitutes variables, and calls Claude.
* @param {string} promptName - The "name" column value to use.
* @param {Object} vars - Values for the prompt's {{placeholders}}.
* @return {string} Claude's reply text.
*/
function ask(promptName, vars = {}) {
// 1. Find the template, or fail loudly if the name is unknown.
const tpl = loadPrompts()[promptName];
if (!tpl) throw new Error('No prompt named: ' + promptName);
// 2. Replace every {{key}} in the template with its value.
const prompt = Object.entries(vars).reduce(
(text, [k, v]) => text.replaceAll('{{' + k + '}}', String(v)),
tpl.template);
// 3. Send the finished prompt to Claude.
return callClaude(prompt, tpl.model);
}
/**
* Minimal Anthropic API call. The key lives in Script Properties — it
* is never pasted into the code.
*/
function callClaude(prompt, model) {
const key = PropertiesService.getScriptProperties()
.getProperty('ANTHROPIC_API_KEY');
const res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
method: 'post',
contentType: 'application/json',
headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
payload: JSON.stringify({
model,
max_tokens: 1000,
messages: [{ role: 'user', content: prompt }],
}),
});
return JSON.parse(res.getContentText()).content[0].text.trim();
}
How it works
loadPromptsfirst checksCacheServicefor apromptsentry. If one is there, it parses and returns it — no spreadsheet read at all.- On a cache miss it reads the
Promptssheet, drops the header row, and skips any blank rows so a stray empty line cannot break the map. - It builds an object keyed by the
namecolumn, each value carrying thetemplatetext and amodel— falling back toDEFAULT_MODELwhen the model column is empty. - It stores that object in the cache for
CACHE_TTLseconds, so a burst of calls within the hour all hit memory instead of the sheet. askcallsloadPrompts, looks up the requested prompt, and throws a clear error if the name does not exist — better a loud failure than a silent one.- It walks the
varsobject and replaces each{{key}}placeholder in the template with the supplied value, then passes the finished prompt tocallClaude. callClaudereads the API key from Script Properties and POSTs the prompt to Anthropic, returning the reply text.
Example run
The Prompts sheet holds:
| name | template | model |
|---|---|---|
| customer-support-reply | Write a friendly reply to a customer about “{{subject}}”. Their message: {{body}} | |
| episode-summary | Summarise this podcast episode in two sentences: {{transcript}} | claude-sonnet-4-6 |
Any script can then call a prompt by name:
function demo() {
const reply = ask('customer-support-reply', {
subject: 'Login issue',
body: 'I cannot reset my password.',
});
Logger.log(reply);
}
ask fills {{subject}} and {{body}}, sends the prompt to Claude Haiku (the
default, since that row’s model column is blank), and logs a finished support
reply. Editing the wording in the sheet changes the output everywhere.
Run it
The library is not run on its own — it is called by your other scripts.
- Add this code to a shared Apps Script project, or copy it into each project that needs prompts.
- Fill the
Promptssheet with your templates. - From any function, call
ask('prompt-name', { ...vars })to get a reply. - To pick up a prompt edit immediately rather than waiting for the cache to
expire, run a one-line function that calls
CacheService.getScriptCache().remove('prompts').
Watch out for
- The cache is the trade-off. Prompt edits take up to
CACHE_TTL(an hour) to appear unless you clear the cache. Lower the TTL while iterating on prompts, raise it once they settle. - Placeholder names must match exactly.
{{body}}in the template only fills if you pass abodykey — a typo leaves the literal{{body}}in the prompt. replaceAlldoes a plain string swap, so unmatched{{...}}tokens pass straight through to Claude. Check your output if a template uses placeholders you did not supply.- Every
askcall is a billable Anthropic request. The cache saves sheet reads, not API calls — there is no caching of Claude’s replies here. - The Script Cache holds at most 100KB per key. A library of very long prompts could exceed that; keep templates reasonable, or split into multiple keys.
Related
Generate and test email subject lines
A/B test AI-written Northwind subject lines for open rate — outputs ranked by past performance.
Updated Mar 3, 2026
Build retrieval-augmented Q&A over your data
Answer Northwind questions grounded in your own Sheet data — pass relevant rows as context.
Updated Feb 27, 2026
Build an AI weekly-report narrator
Turn Northwind metrics into a written executive summary — numbers in, prose out.
Updated Feb 23, 2026
Build a multi-step AI agent workflow
Chain Claude prompts to complete a Northwind task end to end — research → draft → critique → finalise.
Updated Feb 11, 2026
Adapt marketing copy per region
Localise Northwind tone and references by market with AI — same message, regional flavour.
Updated Jan 30, 2026