Hey n8n community!
Sharing a workflow I built to solve a problem I kept running into: spending 2+ hours manually writing social media content every
time I published a blog post.
The workflow monitors your blog’s RSS feed, automatically fetches full article content when a new post is detected, sends it to
OpenAI, and delivers ready-to-review social posts directly to Slack. The free version covers Twitter and LinkedIn — the paid
version extends this to 5 platforms with content calendar logging and Buffer scheduling.
What It Does
Trigger: RSS feed polling (every 30 minutes by default)
Pipeline:
- RSS Trigger detects a new blog post
- Basic input validation (catches empty titles/URLs early)
- HTTP Request fetches the full page HTML
- Code node strips HTML and extracts article text (up to 3000 chars for OpenAI context)
- OpenAI GPT-4o-mini generates a Twitter thread + LinkedIn post, both platform-optimized with hashtags
- Slack delivers everything to your #content channel, formatted and ready to copy-paste
Cost: About $0.001-0.003 per blog post with GPT-4o-mini. Basically free.
Setup (5 minutes)
Credentials you need:
- OpenAI API key — platform.openai.com/api-keys
- Slack Bot Token — api.slack.com/apps (with chat:write scope)
- Your blog’s RSS feed URL — usually yourblog.com/feed or yourblog.com/rss.xml
Steps:
- Import the JSON below into n8n
- Click the RSS Feed Trigger node → enter your blog’s RSS feed URL
- Click Generate Social Posts → create OpenAI credential → paste API key
- Click Slack node → create Slack credential → paste bot token
- Update the channel name to your preferred channel
- Click Test Workflow to verify
- Activate — it runs automatically from here
Workflow JSON
{
“name”: “Blog → Social Posts (AI) — Twitter + LinkedIn → Slack (Free Version)”,
“nodes”: [
{
“parameters”: {
“feedUrl”: “https://yourblog.com/feed”,
“options”: {}
},
“id”: “f2a1b3c4-1001-4e00-b001-000000000004”,
“name”: “RSS Feed Trigger”,
“type”: “n8n-nodes-base.rssFeedReadTrigger”,
“typeVersion”: 1,
“position”: [280, 450],
“polling”: true
},
{
“parameters”: {
“mode”: “runOnceForEachItem”,
“jsCode”: “const item = $input.item.json;\nconst title = item.title || ‘’;\nconst link = item.link || item.url ||
item.guid || ‘’;\nif (!title || !link) throw new Error(‘Missing title or link from RSS feed’);\nreturn { json: { title:
title.trim(), link: link.trim(), description: (item.description || item.summary || ‘’).substring(0, 500) } };”
},
“id”: “f2a1b3c4-1002-4e00-b002-000000000005”,
“name”: “Validate Input”,
“type”: “n8n-nodes-base.code”,
“typeVersion”: 2,
“position”: [560, 450]
},
{
“parameters”: {
“url”: “={{ $json.link }}”,
“options”: { “response”: { “response”: { “fullResponse”: false, “responseFormat”: “text” } }, “timeout”: 15000 }
},
“id”: “f2a1b3c4-1003-4e00-b003-000000000006”,
“name”: “Fetch Blog Content”,
“type”: “n8n-nodes-base.httpRequest”,
“typeVersion”: 4.2,
“position”: [840, 450]
},
{
“parameters”: {
“mode”: “runOnceForEachItem”,
“jsCode”: “const html = $input.item.json.data || $input.item.json.body || ‘’;\nconst title = $items(‘Validate
Input’)[0].json.title;\nconst link = $items(‘Validate Input’)[0].json.link;\nlet content = html;\nconst articleMatch =
html.match(/<article[^>]>([\s\S]?)<\/article>/i);\nconst mainMatch = html.match(/<main[^>]>([\s\S]?)<\/main>/i);\nif
(articleMatch) content = articleMatch[1];\nelse if (mainMatch) content = mainMatch[1];\ncontent =
content.replace(/<script[\s\S]?<\/script>/gi, ‘’).replace(/<style[\s\S]?<\/style>/gi, ‘’).replace(/<[^>]+>/g, ’
').replace(/ /g, ’ ').replace(/\s+/g, ’ ').trim();\nreturn { json: { title, url: link, full_text: content.substring(0,
3000) } };”
},
“id”: “f2a1b3c4-1004-4e00-b004-000000000007”,
“name”: “Extract Article Text”,
“type”: “n8n-nodes-base.code”,
“typeVersion”: 2,
“position”: [1120, 450]
},
{
“parameters”: {
“resource”: “chat”,
“model”: { “__rl”: true, “mode”: “list”, “value”: “gpt-4o-mini” },
“messages”: {
“values”: [
{ “content”: “You are a social media content strategist. Repurpose blog posts into platform-optimized
content.\n\nRules:\n1. Match the tone of the original post\n2. Twitter: max 280 chars per tweet, thread of 3-5 tweets\n3.
LinkedIn: professional, 150-300 words, line breaks\n4. Include 5-8 relevant hashtags per platform\n5. Include the blog URL
naturally\n\nRespond ONLY with valid JSON. No markdown.”, “role”: “system” },
{ “content”: “=Repurpose this blog post:\n\nTitle: {{ $json.title }}\nURL: {{ $json.url }}\nContent:\n{{
$json.full_text }}\n\nGenerate JSON:\n{“twitter_thread”:[“tweet1”,“tweet2”,“tweet3”],“linkedin_post”:“text”,“hasht
ags”:{“twitter”:,“linkedin”:}}”, “role”: “user” }
]
},
“options”: { “temperature”: 0.7, “maxTokens”: 1500 }
},
“id”: “f2a1b3c4-2001-4e00-c001-000000000008”,
“name”: “Generate Social Posts”,
“type”: “@n8n/n8n-nodes-langchain.openAi”,
“typeVersion”: 1.8,
“position”: [1400, 450],
“credentials”: { “openAiApi”: { “id”: “1”, “name”: “OpenAI Account” } }
},
{
“parameters”: {
“mode”: “runOnceForEachItem”,
“jsCode”: “const raw = $input.item.json.message?.content || ‘’;\nlet parsed;\ntry { parsed =
JSON.parse(raw.replace(/json?\\n?/g, '').replace(//g, ‘’).trim()); } catch (e) { throw new Error('Failed to parse OpenAI
response: ’ + e.message); }\nconst title = $items(‘Extract Article Text’)[0].json.title;\nconst url = $items(‘Extract Article
Text’)[0].json.url;\nif (!parsed.twitter_thread) parsed.twitter_thread = ['Check out: ’ + title + ’ ’ + url];\nif
(!parsed.linkedin_post) parsed.linkedin_post = title + '\n\nRead more: ’ + url;\nreturn { json: { …parsed, title, url } };”
},
“id”: “f2a1b3c4-2002-4e00-c002-000000000009”,
“name”: “Parse AI Response”,
“type”: “n8n-nodes-base.code”,
“typeVersion”: 2,
“position”: [1680, 450]
},
{
“parameters”: {
“mode”: “runOnceForEachItem”,
“jsCode”: “const data = $input.item.json;\nconst tweets = Array.isArray(data.twitter_thread) ?
data.twitter_thread.join(‘\n—\n’) : data.twitter_thread;\nconst hashtags = data.hashtags || {};\nconst msg = :memo: *New Social Posts Generated*\\n\\n*Blog:* <${data.url}|${data.title}>\\n\\n---\\n*Twitter Thread:*\\n${tweets}\\n +
(hashtags.twitter?.length ? _Hashtags: ${hashtags.twitter.join(' ')}_\\n : ‘’) +
\\n---\\n*LinkedIn:*\\n${data.linkedin_post}\\n + (hashtags.linkedin?.length ? _Hashtags: ${hashtags.linkedin.join(' ')}_\\n
: ‘’) + \\n---\\n_Upgrade for 5 platforms + scheduling_;\nreturn { json: { slack_message: msg } };”
},
“id”: “f2a1b3c4-3001-4e00-d001-000000000010”,
“name”: “Format Slack Message”,
“type”: “n8n-nodes-base.code”,
“typeVersion”: 2,
“position”: [1960, 450]
},
{
“parameters”: {
“select”: “channel”,
“channelId”: { “__rl”: true, “mode”: “name”, “value”: “#content” },
“text”: “={{ $json.slack_message }}”,
“otherOptions”: {}
},
“id”: “f2a1b3c4-3002-4e00-d002-000000000011”,
“name”: “Slack: Social Posts Ready”,
“type”: “n8n-nodes-base.slack”,
“typeVersion”: 2.4,
“position”: [2240, 450],
“credentials”: { “slackApi”: { “id”: “2”, “name”: “Slack Account” } }
}
],
“pinData”: {},
“connections”: {
“RSS Feed Trigger”: { “main”: [[{“node”: “Validate Input”, “type”: “main”, “index”: 0}]] },
“Validate Input”: { “main”: [[{“node”: “Fetch Blog Content”, “type”: “main”, “index”: 0}]] },
“Fetch Blog Content”: { “main”: [[{“node”: “Extract Article Text”, “type”: “main”, “index”: 0}]] },
“Extract Article Text”: { “main”: [[{“node”: “Generate Social Posts”, “type”: “main”, “index”: 0}]] },
“Generate Social Posts”: { “main”: [[{“node”: “Parse AI Response”, “type”: “main”, “index”: 0}]] },
“Parse AI Response”: { “main”: [[{“node”: “Format Slack Message”, “type”: “main”, “index”: 0}]] },
“Format Slack Message”: { “main”: [[{“node”: “Slack: Social Posts Ready”, “type”: “main”, “index”: 0}]] }
},
“active”: false,
“settings”: {“executionOrder”: “v1”},
“tags”:
}
Customization Ideas
- Change the AI model: Swap gpt-4o-mini for gpt-4o in the OpenAI node for higher quality output (costs more but noticeably
better) - Filter by category: Add an IF node after the RSS trigger to only process posts tagged with certain categories
- Change the polling interval: The RSS trigger defaults to 30 minutes — you can set it to 15 minutes, 1 hour, or daily
- Add more platforms: Edit the system prompt to request Instagram, Facebook, or newsletter snippet formats
Full Version
I also have a full version with a few more features that weren’t worth cramming into a free template:
- 5 platforms instead of 2 (adds Instagram caption, Facebook post, Newsletter snippet)
- Duplicate detection — tracks processed URLs in workflow static data, never reprocesses the same post
- AI fallback path — if OpenAI fails or returns invalid JSON, generates basic posts from the excerpt so the pipeline never
breaks - Google Sheets logging — appends a content calendar row for every post (date, title, URL, all 5 posts, hashtags, image prompt)
- Buffer integration — optionally queues Twitter and LinkedIn posts via Buffer API for scheduled publishing
- Full error handling — all nodes have retry logic, and any failure sends a Slack alert to #alerts
- Manual trigger — run it on demand for any URL, not just from RSS
Available at: https://flowyantra.gumroad.com/l/blog-to-social-ai
Also on GitHub for the free version: https://github.com/flowyantra/blog-to-social-ai-n8n
Hope this is useful — happy to answer questions or help if you hit issues with the setup!

