Spin a blog post into social captions
Generate platform-specific posts from one Northwind article — LinkedIn, X, Threads.
Published Aug 11, 2025
Northwind publishes a blog post, and then someone has to turn it into social captions — a polished LinkedIn version, a punchy line for X, something conversational for Threads. Each platform wants a different length and tone, so it is three separate writing jobs on top of the article itself. The captions usually get rushed, or skipped.
This function does the rewriting. Point it at the Google Doc that holds a blog post and it asks Claude for three captions in one pass — each tuned to its platform — and hands them back as structured data you can paste straight into a scheduler. The article ships with its captions already written.
What you’ll need
- A blog post written in a Google Doc — you’ll pass its document ID to the function.
- An Anthropic API key saved as
ANTHROPIC_API_KEYin Script Properties — see Store API keys and secrets securely. - Nothing else — this is a single function you call with a doc ID.
The script
// Per-platform caption specs. Edit these to retune length or tone
// without touching the prompt-building code below.
const CAPTION_SPECS = [
{ key: 'linkedin', brief: 'One LinkedIn post (200 words, professional tone)' },
{ key: 'x', brief: 'One X post (270 chars, punchy)' },
{ key: 'threads', brief: 'One Threads post (300 chars, conversational)' },
];
/**
* Reads a Northwind blog post from a Google Doc and asks Claude to
* rewrite it as one caption per platform.
*
* @param {string} blogDocId - The Google Doc ID of the blog post.
* @return {Object} An object keyed by platform: {linkedin, x, threads}.
*/
function spinSocialCaptions(blogDocId) {
// 1. Pull the full text of the blog post out of the doc.
const body = DocumentApp.openById(blogDocId).getBody().getText().trim();
if (!body) {
Logger.log('The doc is empty — nothing to spin.');
return {};
}
// 2. Build a bulleted brief from the caption specs.
const briefs = CAPTION_SPECS.map((s) => '- ' + s.brief).join('\n');
// 3. Ask for strict JSON keyed by platform — one pass, three captions.
const prompt =
'Distil this Northwind blog post into:\n' + briefs + '\n\n' +
'Return ONLY a JSON object — no prose, no markdown — in this shape: ' +
'{"linkedin": string, "x": string, "threads": string}.\n\n' +
body;
// 4. Sonnet does the rewriting; strip any code fence, then parse.
const reply = callClaude(prompt);
const captions = JSON.parse(stripFences(reply));
Logger.log('Generated captions for: ' + Object.keys(captions).join(', '));
return captions;
}
/**
* Claude occasionally wraps JSON in a ```json code fence. Strip it so
* JSON.parse never chokes on the markdown.
*/
function stripFences(text) {
return text.replace(/```(?:json)?/g, '').trim();
}
/**
* Minimal Anthropic API call. The key lives in Script Properties — it
* is never pasted into the code.
*/
function callClaude(prompt, model = 'claude-sonnet-4-6', maxTokens = 800) {
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: maxTokens,
messages: [{ role: 'user', content: prompt }],
}),
muteHttpExceptions: true,
});
return JSON.parse(res.getContentText()).content[0].text.trim();
}
How it works
spinSocialCaptionsopens the Google Doc by ID and pulls out its full text.- If the doc is empty, it logs a message and returns an empty object — no wasted API call.
- It turns
CAPTION_SPECSinto a bulleted brief, so retuning a platform’s length or tone is a one-line edit at the top of the file. - It builds a prompt that pins the output to a strict JSON object keyed by
platform —
linkedin,x,threads. - It calls Claude Sonnet, which handles tone-shifting well.
stripFencesremoves any code fence, thenJSON.parseturns the reply into real objects. - It returns the captions object so the caller can paste them, log them, or write them into a scheduling sheet.
Example run
Call it from the editor or another function with a doc ID:
const captions = spinSocialCaptions('1AbcBlogDocId');
Logger.log(captions.x);
For a blog post titled “Five ways to speed up Northwind onboarding”, the returned object looks like this:
| Platform | Caption |
|---|---|
linkedin | ”Onboarding is the first impression your product makes. We cut Northwind’s setup time in half with five small changes — clearer defaults, a guided first task, fewer required fields… (200 words)“ |
x | ”Slow onboarding loses customers before they ever start. 5 fixes that cut Northwind setup time in half 🧵“ |
threads | ”We timed our own onboarding and it was painful. Here’s what we changed — and why the smallest fix made the biggest difference.” |
Three ready-to-schedule captions from one article, no extra writing session.
Run it
This is an on-demand job — you run it when a post is ready, not on a schedule:
- In the Apps Script editor, open a function that calls
spinSocialCaptionswith your doc ID, or call it from the editor’s console. - Approve the authorisation prompt the first time.
- Read the captions from the log, or wire the return value into a sheet.
To make it self-serve from a doc, add a custom menu that prompts for the ID:
function onOpen() {
DocumentApp.getUi()
.createMenu('Northwind tools')
.addItem('Spin social captions', 'runFromActiveDoc')
.addToUi();
}
function runFromActiveDoc() {
const captions = spinSocialCaptions(DocumentApp.getActiveDocument().getId());
DocumentApp.getUi().alert(JSON.stringify(captions, null, 2));
}
Watch out for
- Character limits are guidance, not guarantees. Claude usually respects “270 chars”, but check the X caption before posting — count it in code if the caption goes straight to an API that will reject an over-length post.
- Very long articles can exceed the model’s input budget. For a long-form post, summarise it to its key points first, then spin captions from the summary.
- Strict JSON keeps this reliable.
stripFenceshandles the common case where Claude wraps the object in a code fence. IfJSON.parsestill throws, log the raw reply and tighten the prompt rather than reaching for regex. - The captions reflect the article’s tone. If the blog post is dry, the captions
will be too — edit the platform briefs in
CAPTION_SPECSto push for more energy if you need it.
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