{
  "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
          }
        ]
      ]
    }
  }
}