This workflow corresponds to n8n.io template #7974 — we link there as the canonical source.
This workflow follows the Slack → Slack Trigger recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "sbb4vrO0WsNieVjJ",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "My workflow",
"tags": [],
"nodes": [
{
"id": "7506a331-0426-43ca-86ee-c912e10312c7",
"name": "RSS Read",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
384,
176
],
"parameters": {
"url": "https://skift.com/feed/",
"options": {}
},
"typeVersion": 1.2
},
{
"id": "f15bc30b-b95c-4a15-8a1d-b249b15713de",
"name": "RSS Read1",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
368,
-16
],
"parameters": {
"url": "https://news.google.com/rss/search?q=hotel+technology+OR+hospitality+tech+when:7d&hl=en-US&gl=US&ceid=US:en",
"options": {}
},
"typeVersion": 1.2
},
{
"id": "02bacaaa-a811-4362-9618-85e5818a7fc7",
"name": "RSS Read2",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
368,
-240
],
"parameters": {
"url": "https://news.google.com/rss/search?q=YOUR_AWS_SECRET_KEY_HERE+when:7d&hl=en-US&gl=US&ceid=US:en",
"options": {}
},
"typeVersion": 1.2
},
{
"id": "c2385ad6-4fac-4643-9d41-28ae2f5d4730",
"name": "Slack: Incoming messages",
"type": "n8n-nodes-base.slackTrigger",
"position": [
-400,
-16
],
"parameters": {
"options": {},
"trigger": [
"message"
],
"channelId": {
"__rl": true,
"mode": "id",
"value": "C09C3Q4QCF3"
}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "ff12ac88-4c97-4503-b6b3-c5f76ca10962",
"name": "Code: parse_slack_command",
"type": "n8n-nodes-base.code",
"position": [
-128,
-16
],
"parameters": {
"jsCode": "// --- Slack Event Normalizer ---\n// Works with Slack Trigger (test) or Production webhook body\n\nconst ev =\n $json.event ??\n $json.body?.event ?? // prod webhook\n $json; // fallback (test)\n\nif (!ev) {\n console.log('No Slack event in payload:', $json);\n return [];\n}\n\n// Ignore bot/system messages and edits\nif (ev.bot_id || ev.subtype === 'bot_message' || ev.subtype === 'message_changed') {\n return [];\n}\n\n// Strip a leading @mention like \"<@U123...> \"\nlet text = (ev.text || '').replace(/^<@[^>]+>\\s*/g, '').trim();\nconst lower = text.toLowerCase();\n\nlet cmd = null, pick = null, notes = null;\n\n// start/stop/done/revise\nif (lower === 'start') {\n cmd = 'start';\n} else if (lower === 'stop') {\n cmd = 'stop';\n} else if (lower === 'done') {\n cmd = 'done';\n} else if (/^revise\\[(.*)\\]$/i.test(text)) {\n cmd = 'revise';\n notes = text.match(/^revise\\[(.*)\\]$/i)[1].trim();\n}\n\n// gen (supports \"2\" or \"gen 2\")\nconst m = lower.match(/^gen\\s*(\\d+)$/i);\nif (!cmd && m) {\n cmd = 'gen';\n pick = parseInt(m[1], 10);\n} else if (!cmd && /^\\d+$/.test(lower)) {\n cmd = 'gen';\n pick = parseInt(lower, 10);\n}\n\nreturn [{\n json: {\n cmd,\n pick,\n notes,\n channel: ev.channel || $json.channel,\n ts: ev.ts || $json.ts,\n thread_ts: ev.thread_ts || ev.ts || $json.thread_ts || $json.ts,\n user: ev.user || $json.user,\n text,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "c10ea145-13b8-4f8f-bc68-301dbbb3353e",
"name": "Switch: route_by_command",
"type": "n8n-nodes-base.switch",
"position": [
80,
-32
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "92dd4e01-59ac-4266-985c-34b05f044bd6",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{$json.cmd}}",
"rightValue": "start"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "fa91281f-0b2f-4ac3-99b5-c35fa856ee96",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{$json.cmd}}",
"rightValue": "gen"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1814a166-2e90-45cf-8fbd-acaadfc0a1fd",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{$json.cmd}}",
"rightValue": "revise"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f004378f-f978-45dd-b507-337132ec4aaf",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{$json.cmd}}",
"rightValue": "done"
}
]
}
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "348766c0-42ad-4f96-888b-8ef20bd2261b",
"name": "Merge: all_feeds",
"type": "n8n-nodes-base.merge",
"position": [
656,
0
],
"parameters": {
"numberInputs": 3
},
"typeVersion": 3.2
},
{
"id": "a516595a-76ae-45b9-b654-7e547a31af61",
"name": "Code: make_headline_payload",
"type": "n8n-nodes-base.code",
"position": [
864,
0
],
"parameters": {
"jsCode": "/**\n * Code1 \u2014 after Merge (Append)\n * Build clean, deduped article list and a compact text block for Gemini.\n */\n\n// ====== configuration ======\nconst MAX_HEADLINES = 60; // <= how many headlines to pass to Gemini (try 50\u201380)\n\n// ====== helpers ======\nfunction stripHtml(html = '') {\n // Remove tags\n const noTags = String(html).replace(/<[^>]*>/g, ' ');\n // Collapse whitespace\n return noTags.replace(/\\s+/g, ' ').trim();\n}\n\nfunction truncate(str = '', n = 140) {\n const s = String(str).trim();\n return s.length > n ? s.slice(0, n) + '\u2026' : s;\n}\n\nfunction pickFirst(obj, keys) {\n for (const k of keys) {\n if (obj?.[k]) return obj[k];\n }\n return undefined;\n}\n\n// ====== collect & normalize ======\nconst items = $input.all(); // all merged items\nconst seen = new Set(); // dedupe by *normalized title*\nconst articles = [];\n\nfor (const item of items) {\n const j = item.json || {};\n\n // Try common RSS/Atom/HTTP node fields\n const rawTitle = pickFirst(j, [\n 'title', 'headline', 'name'\n ]);\n\n // If no title, skip (we need a key to dedupe and display)\n if (!rawTitle) continue;\n\n // For descriptions, try several fields and strip HTML\n const rawDesc = pickFirst(j, [\n 'contentSnippet', 'description', 'summary', 'content', 'content:encoded'\n ]);\n const description = rawDesc ? stripHtml(rawDesc) : '';\n\n // Links/guid\n const link = pickFirst(j, [\n 'link', 'url', 'guid'\n ]);\n\n // Deduplicate by normalized title\n const title = String(rawTitle).trim();\n const key = title.toLowerCase();\n if (seen.has(key)) continue;\n seen.add(key);\n\n articles.push({\n title,\n description,\n link: link ? String(link).trim() : ''\n });\n}\n\n// ====== choose how much to send to Gemini ======\nconst limited = articles.slice(0, MAX_HEADLINES);\n\n// Build compact arrays for Gemini + debugging\nconst headlines = limited.map(a => a.title);\n\n// If you want titles + a short blurb in the prompt, use this:\nconst articleText = limited\n .map((a, i) => `${i + 1}. ${a.title}${a.description ? ` \u2014 ${truncate(a.description)}` : ''}`)\n .join('\\n');\n\n// ====== output ======\nreturn [{\n json: {\n articles: limited, // array of normalized items\n headlines, // array of titles only\n articleText, // numbered list for Gemini\n articleCount: limited.length,\n }\n}];"
},
"typeVersion": 2
},
{
"id": "4d5de947-36b0-4d5b-9bdc-708ad24ab6aa",
"name": "Gemini: cluster_to_topics",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
1168,
0
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-1.5-flash",
"cachedResultName": "models/gemini-1.5-flash"
},
"options": {},
"messages": {
"values": [
{
"content": "=You are a hotel-industry editor.\n\nYou are given recent hospitality headlines & snippets:\n{{$json.articleText}}\n\nTask:\n- Select 10\u201330 of the most relevant, diverse themes.\n- For each theme, write a short, **paraphrased headline** in your own words (do NOT copy any headline verbatim).\n- Keep each headline concise and specific (\u2264 120 characters).\n- No emojis, no hashtags, no numbering, no quotes.\n\nReturn STRICT JSON ONLY, exactly in this shape:\n{\n \"topics\": [\n { \"topic\": \"Paraphrased headline 1\" },\n { \"topic\": \"Paraphrased headline 2\" }\n ]\n}"
}
]
},
"jsonOutput": true
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "e226a282-4ac1-4e7d-86c5-a2c69bd3736a",
"name": "Code: parse_or_pass_topics",
"type": "n8n-nodes-base.code",
"position": [
1568,
0
],
"parameters": {
"jsCode": "// Code2 \u2014 normalize Gemini topics JSON for Slack\n\nconst raw =\n $json.text ??\n $json.content?.parts?.[0]?.text ??\n '';\n\nlet out = { topics: [] };\n\ntry {\n const cleaned = String(raw).replace(/```json|```/gi, '').trim();\n\n // try full first, then last {...}\n try {\n out = JSON.parse(cleaned);\n } catch {\n const m = cleaned.match(/\\{[\\s\\S]*\\}$/);\n if (m) out = JSON.parse(m[0]);\n }\n} catch (e) {\n console.log('Failed to parse Gemini output:', e, String(raw).slice(0, 300));\n out = { topics: [] };\n}\n\n// --- normalize to [{topic: \"...\"}]\nlet topics = out?.topics ?? [];\nif (!Array.isArray(topics)) topics = [];\n\ntopics = topics\n .map(t => {\n if (typeof t === 'string') return { topic: t.trim() };\n if (t && typeof t === 'object') {\n return { topic: String(t.topic ?? t.headline ?? t.title ?? '').trim() };\n }\n return { topic: '' };\n })\n .filter(t => t.topic);\n\n// guardrail: de-dup & cap (optional)\nconst seen = new Set();\ntopics = topics.filter(t => {\n const key = t.topic.toLowerCase();\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n});\n\n// output to next nodes\nreturn [{\n json: {\n mode: 'topics',\n topics\n }\n}];"
},
"typeVersion": 2
},
{
"id": "a84fa8a6-231d-443b-bbcd-72360a014980",
"name": "Slack: post_topic_list",
"type": "n8n-nodes-base.slack",
"position": [
2112,
-96
],
"parameters": {
"text": "={{ (function () {\n const t = $json.topics || [];\n if (!Array.isArray(t) || !t.length) {\n return 'No topics found. Try `start` again.';\n }\n\n // Show up to 30 items (or all)\n const lines = t.map((x, i) => `${i + 1}) ${x.topic}`);\n const body = lines.join('\\n');\n\n return `Trending hotel topics (reply with a number, e.g. 2):\\n\\n${body}\\n\\nThen: \\`revise[ your notes ]\\` or \\`done\\`.\\nAutomated with this n8n workflow`;\n})() }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "C09C3Q4QCF3"
},
"otherOptions": {
"thread_ts": {
"replyValues": {
"thread_ts": "={{$node[\"Code: parse_slack_command\"].json.thread_ts || $node[\"Code: parse_slack_command\"].json.ts}}"
}
}
}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "56c5667f-18d7-401b-b300-cf7429eef2a3",
"name": "Slack: fetch_thread_replies",
"type": "n8n-nodes-base.slack",
"position": [
432,
416
],
"parameters": {
"ts": "={{$node[\"Code: parse_slack_command\"].json.thread_ts || $node[\"Code: parse_slack_command\"].json.ts}}",
"filters": {},
"resource": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "C09C3Q4QCF3"
},
"operation": "replies"
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
},
{
"id": "36d792cc-bdc2-4de8-aa3e-fca5d7d9750a",
"name": "Code: pick_topic_from_thread",
"type": "n8n-nodes-base.code",
"position": [
1056,
432
],
"parameters": {
"jsCode": "// post gen \u2014 map user number to the chosen headline from the thread\n\n// 1) get the user's pick from anywhere we can\nfunction num(x) {\n const n = Number(x);\n return Number.isFinite(n) ? n : undefined;\n}\n\nlet pick =\n num($json.pick) ??\n num($node[\"Code: parse_slack_command\"].json?.pick) ?? // from your normalizer Code\n num(($node[\"Code: parse_slack_command\"].json?.text || \"\").match(/\\d+/)?.[0]); // last resort\n\nif (!pick) {\n return [{ json: { error: 'no_pick', msg: \"Reply with a number like 2 or 'gen 2'.\" } }];\n}\n\n// 2) normalize Slack replies shape to an array of message objects\n// - Return All OFF: slack node gives { messages: [...] }\n// - Return All ON: slack node gives many items, one per message\nlet msgs = [];\nif (Array.isArray($json.messages)) {\n msgs = $json.messages;\n} else if (Array.isArray($items().map(i => i.json))) {\n // Return All ON case: collect all incoming items as messages\n msgs = $items().map(i => i.json);\n}\n\n// 3) find the bot message that listed the topics\n// Adapt the marker below to the exact first line you print in Slack\nconst LIST_MARKER = 'Trending hotel topics';\nlet listMsg = null;\n\n// try newest-first if provided by Slack, else do a defensive search\nfor (const m of msgs) {\n if (typeof m.text === 'string' && m.text.includes(LIST_MARKER)) {\n listMsg = m;\n break;\n }\n}\nif (!listMsg) {\n for (let i = msgs.length - 1; i >= 0; i--) {\n const m = msgs[i];\n if (typeof m.text === 'string' && m.text.includes(LIST_MARKER)) {\n listMsg = m;\n break;\n }\n }\n}\nif (!listMsg) {\n return [{ json: { error: 'no_list', msg: 'Could not find the topics list message in this thread.' } }];\n}\n\n// 4) parse numbered lines from the list message\n// We accept both \"1) Title (12)\" and \"1) Title\" styles\nconst lines = (listMsg.text || '').split('\\n');\nconst topics = [];\nfor (const line of lines) {\n // capture: number ) <headline> (optional (count))\n const m = line.match(/^\\s*\\d+\\)\\s+(.+?)(?:\\s+\\(\\d+\\))?\\s*$/);\n if (m) topics.push(m[1].trim());\n}\n\nif (!topics.length) {\n return [{ json: { error: 'no_topics', msg: 'Could not parse any topics from the list message.' } }];\n}\n\n// clamp pick to available range\nconst idx = Math.max(1, Math.min(pick, topics.length)) - 1;\nconst topic = topics[idx];\n\n// 5) pass forward for Gemini post generation\nreturn [{\n json: {\n topic,\n pick: idx + 1,\n topicsCount: topics.length,\n channel: $node[\"Code: parse_slack_command\"].json?.channel,\n thread_ts: $node[\"Code: parse_slack_command\"].json?.thread_ts || $node[\"Code: parse_slack_command\"].json?.ts\n }\n}];"
},
"typeVersion": 2
},
{
"id": "bf46b6cb-b1a2-4c48-8095-0a0297b9dddb",
"name": "Gemini: write_draft",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"position": [
1504,
336
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "models/gemini-1.5-flash",
"cachedResultName": "models/gemini-1.5-flash"
},
"options": {},
"messages": {
"values": [
{
"content": "=You are a hotel-industry content writer.\nWrite a 250\u2013350 word LinkedIn-style post about:\n\n\u201c{{$json.topic}}\u201d\n\nUse insights from these recent headlines (signal, not quotes):\n{{$json.articleText || ''}}\n\nRequirements:\n- Start with a strong hook (1\u20132 sentences).\n- Include 3\u20135 concise bullets with specific, non-generic observations.\n- End with a one-sentence takeaway + a soft CTA.\n- Confident, analytical tone. No hashtags. No emojis.\n- Plain text only."
}
]
}
},
"credentials": {
"googlePalmApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "94761e71-40c5-4cfd-a127-3508489d331d",
"name": "Slack: post_draft",
"type": "n8n-nodes-base.slack",
"position": [
2112,
160
],
"parameters": {
"text": "={{$json.text || $json.content?.parts?.[0]?.text || 'No content generated.'}}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "C09C3Q4QCF3"
},
"otherOptions": {
"thread_ts": {
"replyValues": {
"thread_ts": "={{$node[\"Code: parse_slack_command\"].json.thread_ts || $node[\"Code: parse_slack_command\"].json.ts}}"
}
}
}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.3
}
],
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "bb356e18-0314-47a2-bbf5-eb471741b079",
"connections": {
"RSS Read": {
"main": [
[
{
"node": "Merge: all_feeds",
"type": "main",
"index": 2
}
]
]
},
"RSS Read1": {
"main": [
[
{
"node": "Merge: all_feeds",
"type": "main",
"index": 1
}
]
]
},
"RSS Read2": {
"main": [
[
{
"node": "Merge: all_feeds",
"type": "main",
"index": 0
}
]
]
},
"Merge: all_feeds": {
"main": [
[
{
"node": "Code: make_headline_payload",
"type": "main",
"index": 0
}
]
]
},
"Gemini: write_draft": {
"main": [
[
{
"node": "Slack: post_draft",
"type": "main",
"index": 0
}
]
]
},
"Slack: post_topic_list": {
"main": [
[]
]
},
"Slack: Incoming messages": {
"main": [
[
{
"node": "Code: parse_slack_command",
"type": "main",
"index": 0
}
]
]
},
"Switch: route_by_command": {
"main": [
[
{
"node": "RSS Read2",
"type": "main",
"index": 0
},
{
"node": "RSS Read1",
"type": "main",
"index": 0
},
{
"node": "RSS Read",
"type": "main",
"index": 0
}
],
[
{
"node": "Slack: fetch_thread_replies",
"type": "main",
"index": 0
}
]
]
},
"Code: parse_slack_command": {
"main": [
[
{
"node": "Switch: route_by_command",
"type": "main",
"index": 0
}
]
]
},
"Gemini: cluster_to_topics": {
"main": [
[
{
"node": "Code: parse_or_pass_topics",
"type": "main",
"index": 0
}
]
]
},
"Code: parse_or_pass_topics": {
"main": [
[
{
"node": "Slack: post_topic_list",
"type": "main",
"index": 0
}
]
]
},
"Code: make_headline_payload": {
"main": [
[
{
"node": "Gemini: cluster_to_topics",
"type": "main",
"index": 0
}
]
]
},
"Slack: fetch_thread_replies": {
"main": [
[
{
"node": "Code: pick_topic_from_thread",
"type": "main",
"index": 0
}
]
]
},
"Code: pick_topic_from_thread": {
"main": [
[
{
"node": "Gemini: write_draft",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googlePalmApislackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow turns Slack into your content control hub and automates the full blog creation pipeline — from sourcing trending headlines, validating topics, drafting posts, and preparing content for your CMS.
Source: https://n8n.io/workflows/7974/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow helps you repurpose your YouTube videos across multiple social media platforms with zero manual effort. It’s designed for creators, businesses, and marketers who want to maximize reach w
This workflow empowers marketing teams, agencies and solopreneurs to instantly generate on-brand, platform-optimized social media ads — without designers or complex setup. Running performance marketin
This workflow helps you capture "tribal knowledge" shared in Slack conversations and automatically converts it into structured documentation. By simply adding a specific reaction (default: 📚) to a mes
> This n8n workflow template uses a community node and is only compatible with the self-hosted version of n8n.
This workflow turns brand mentions into a lively “personality analysis” — making your reports not only insightful but also fun to read. Perfect for teams that want to stay informed and entertained.