{
  "id": "a6nwY85e9GX2Y24b",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Reddit Thread \u2192 AI Summary \u2192 Social Media Publisher (X + LinkedIn)",
  "tags": [],
  "nodes": [
    {
      "id": "49e47b33-6ce0-4868-b2ac-46d58de66710",
      "name": "Reddit URL Input",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -336,
        32
      ],
      "parameters": {
        "options": {},
        "formTitle": "Reddit Thread Post Generator",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Reddit Thread URL"
            }
          ]
        },
        "formDescription": "generates social media posts based on reddit thread"
      },
      "typeVersion": 2.5
    },
    {
      "id": "685f7307-665f-4fd5-aeb6-b9f107b73279",
      "name": "Parse Reddit URL",
      "type": "n8n-nodes-base.code",
      "position": [
        -48,
        32
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Parse Reddit Thread URL \u2192 extract subreddit + post ID\n// Supports: reddit.com/r/sub/comments/id, redd.it/id, mobile URLs\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst rawUrl = $input.first().json['Reddit Thread URL'].trim();\n\n// Normalize: remove trailing slash, www, m. prefix\nconst url = rawUrl.replace(/^(https?:\\/\\/)?(www\\.|m\\.)?/, 'https://');\n\n// Match standard thread URL\nconst fullMatch = url.match(/reddit\\.com\\/r\\/([^/]+)\\/comments\\/([a-zA-Z0-9]+)/);\n\nif (!fullMatch) {\n  throw new Error(\n    '\u274c Invalid Reddit URL. Please use a standard thread URL like: ' +\n    'https://www.reddit.com/r/subreddit/comments/postId/title/'\n  );\n}\n\nconst subreddit = fullMatch[1];\nconst postId = fullMatch[2];\nconst apiUrl = `https://www.reddit.com/r/${subreddit}/comments/${postId}.json`;\nconst cleanThreadUrl = `https://www.reddit.com/r/${subreddit}/comments/${postId}/`;\n\nreturn [{\n  json: {\n    originalUrl: rawUrl,\n    subreddit,\n    postId,\n    apiUrl,\n    cleanThreadUrl\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "e8af925a-9bc6-4538-83dc-6e15cbe98386",
      "name": "Fetch Reddit Thread",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        256,
        32
      ],
      "parameters": {
        "url": "={{ $json.apiUrl }}",
        "options": {},
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "limit",
              "value": "100"
            },
            {
              "name": "depth",
              "value": "3"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "n8n-workflow-automation/1.0"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "27751aa7-a183-4791-890a-b0f55352cf87",
      "name": "Extract Thread Content",
      "type": "n8n-nodes-base.code",
      "position": [
        544,
        32
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Extract post + ALL comments from Reddit JSON API response\n// \u2022 Top-level comments sorted by score (highest first)\n// \u2022 Each comment's replies also sorted by score (highest first)\n// \u2022 Full depth traversal \u2014 no depth limit\n// \u2022 Structured output: nested objects + flat string for AI\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst response = $input.first().json;\n\n// \u2500\u2500 Handle both: array-as-single-item OR multiple items from HTTP node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet postListing, commentsListing;\n\nif (Array.isArray(response)) {\n  postListing     = response[0];\n  commentsListing = response[1];\n} else {\n  const allItems = $input.all();\n  if (allItems.length >= 2) {\n    postListing     = allItems[0].json;\n    commentsListing = allItems[1].json;\n  } else {\n    throw new Error('Unexpected Reddit API response. Could not parse thread data.');\n  }\n}\n\n// \u2500\u2500 Extract post metadata \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst pd = postListing.data.children[0].data;\n\nconst post = {\n  title:       pd.title        || '',\n  body:        pd.selftext     ? pd.selftext.trim() : '',\n  author:      pd.author       || '[deleted]',\n  score:       pd.score        || 0,\n  upvoteRatio: pd.upvote_ratio || null,\n  subreddit:   pd.subreddit    || '',\n  numComments: pd.num_comments || 0,\n  threadUrl:   `https://www.reddit.com${pd.permalink}`,\n  isLinkPost:  pd.is_self === false,\n  externalUrl: pd.is_self === false ? pd.url : null,\n  createdUtc:  pd.created_utc  || null,\n  awards:      pd.total_awards_received || 0,\n  flair:       pd.link_flair_text || null\n};\n\n// \u2500\u2500 Recursively extract comments \u2014 no depth limit, all replies \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Strategy:\n//   1. At each level, sort siblings by score DESC\n//   2. Recurse into every reply thread fully\n//   3. Skip \"more\" stubs (kind: 'more') \u2014 these are collapsed Reddit nodes\n//      that would need additional API calls to expand\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction extractComments(children, depth) {\n  if (!children || children.length === 0) return [];\n\n  // Filter to real comments only (kind 't1'), skip 'more' load stubs\n  const realComments = children.filter(c => c.kind === 't1');\n\n  // Sort by score descending at this level\n  realComments.sort((a, b) => (b.data.score || 0) - (a.data.score || 0));\n\n  const result = [];\n\n  for (const child of realComments) {\n    const c = child.data;\n\n    const body = (c.body || '').replace(/\\n+/g, ' ').trim();\n    if (!body || body === '[deleted]' || body === '[removed]') continue;\n\n    // Build structured comment object\n    const comment = {\n      id:         c.id     || '',\n      author:     c.author || '[deleted]',\n      score:      c.score  || 0,\n      body:       body,\n      depth:      depth,\n      awards:     c.total_awards_received || 0,\n      createdUtc: c.created_utc || null,\n      isEdited:   !!c.edited,\n      replies:    []\n    };\n\n    // Recurse into replies with no depth cap\n    if (\n      c.replies &&\n      c.replies !== '' &&\n      c.replies.data &&\n      c.replies.data.children &&\n      c.replies.data.children.length > 0\n    ) {\n      comment.replies = extractComments(c.replies.data.children, depth + 1);\n    }\n\n    result.push(comment);\n  }\n\n  return result;\n}\n\nconst structuredComments = extractComments(commentsListing.data.children, 0);\n\n// \u2500\u2500 Count totals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nfunction countAll(comments) {\n  let total = 0;\n  for (const c of comments) {\n    total += 1 + countAll(c.replies);\n  }\n  return total;\n}\n\nconst totalExtracted = countAll(structuredComments);\n\n// \u2500\u2500 Build flat string for AI consumption \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Format:\n//   [SCORE] u/author: comment text\n//       \u21b3 [SCORE] u/author: reply text          (depth 1)\n//           \u21b3 [SCORE] u/author: sub-reply text  (depth 2)\n//\n// Indentation visually represents thread nesting depth.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction buildFlatString(comments, lines = []) {\n  for (const c of comments) {\n    const pad    = '    '.repeat(c.depth);\n    const arrow  = c.depth > 0 ? '\u21b3 ' : '';\n    const awards = c.awards > 0 ? ` \ud83c\udfc6\u00d7${c.awards}` : '';\n    lines.push(`${pad}${arrow}[${c.score} pts${awards}] u/${c.author}: ${c.body}`);\n    if (c.replies.length > 0) {\n      buildFlatString(c.replies, lines);\n    }\n  }\n  return lines;\n}\n\nconst commentLines = buildFlatString(structuredComments);\n\n// \u2500\u2500 Assemble threadContent string for downstream AI nodes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet threadContent = `=== REDDIT THREAD ===\\n`;\nthreadContent += `SUBREDDIT   : r/${post.subreddit}\\n`;\nthreadContent += `TITLE       : ${post.title}\\n`;\nthreadContent += `AUTHOR      : u/${post.author}\\n`;\nthreadContent += `SCORE       : ${post.score.toLocaleString()} upvotes`;\nthreadContent += post.upvoteRatio ? ` (${Math.round(post.upvoteRatio * 100)}% upvoted)\\n` : '\\n';\nthreadContent += `COMMENTS    : ${post.numComments.toLocaleString()} total, ${totalExtracted} extracted\\n`;\nif (post.flair)   threadContent += `FLAIR       : ${post.flair}\\n`;\nif (post.awards)  threadContent += `AWARDS      : ${post.awards}\\n`;\nthreadContent += `URL         : ${post.threadUrl}\\n\\n`;\n\nif (post.body) {\n  threadContent += `POST BODY:\\n${post.body}\\n\\n`;\n} else if (post.isLinkPost) {\n  threadContent += `LINK POST \u2192 ${post.externalUrl}\\n\\n`;\n}\n\nthreadContent += `COMMENTS (sorted by score, highest first \u2014 full depth):\\n`;\nthreadContent += commentLines.join('\\n');\n\nreturn [{\n  json: {\n    // \u2500\u2500 Post metadata\n    ...post,\n\n    // \u2500\u2500 Structured nested comments tree (for any downstream node that needs JSON)\n    structuredComments,\n\n    // \u2500\u2500 Stats\n    totalExtracted,\n    maxDepthFound: structuredComments.length > 0\n      ? Math.max(...commentLines.map((_, i) => {\n          const match = commentLines[i].match(/^(    )*/);\n          return match ? match[0].length / 4 : 0;\n        }))\n      : 0,\n\n    // \u2500\u2500 Flat string ready for AI prompt injection\n    threadContent\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "45a3cf91-1a16-4ff9-a17d-017058aa3de5",
      "name": "Summarize Thread",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        816,
        32
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-3.1-flash-lite-preview",
          "cachedResultName": "models/gemini-3.1-flash-lite-preview"
        },
        "options": {
          "topK": 20,
          "temperature": 0.7,
          "maxToolsIterations": 5
        },
        "messages": {
          "values": [
            {
              "content": "=Analyze the following Reddit thread and generate a comprehensive, structured summary.\n\n{{ $json.threadContent }}\n\n---\n\nPlease return a detailed markdown summary with the following sections:\n\n## \ud83e\uddf5 Thread Overview\nWhat is this thread about? (2\u20133 sentences, include the subreddit context)\n\n## \ud83d\udd11 Key Topics Discussed\nBullet-point list of the main themes and topics in this thread\n\n## \ud83d\udca1 Notable Insights & Perspectives\nThe most interesting, contrarian, or valuable perspectives from commenters. Quote or paraphrase 3\u20135 standout comments.\n\n## \ud83d\udcca Community Sentiment\nOverall sentiment (positive / negative / mixed / neutral) and why. What does the community agree or disagree on?\n\n## \ud83c\udfaf Actionable Takeaways\nList 3\u20135 concrete insights or lessons someone could apply from this discussion.\n\nBe specific and data-driven. Reference actual details from the thread \u2014 usernames, scores, and specific quotes where relevant."
            }
          ]
        },
        "simplify": false,
        "builtInTools": {
          "urlContext": true
        }
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "d2088a1a-1047-4936-880e-dd5dae44286c",
      "name": "Generate Social Posts",
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "position": [
        1200,
        32
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "models/gemini-3.1-flash-lite-preview",
          "cachedResultName": "models/gemini-3.1-flash-lite-preview"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=You are an expert social media content strategist. Based on the Reddit thread summary below, create high-performing, platform-optimized posts for X (Twitter) and LinkedIn.\n\n--- THREAD SUMMARY ---\n{{ $json.candidates[0].content.parts[0].text }}\n--- END SUMMARY ---\n\nCreate two posts:\n\n1. **X (Twitter) Post** \u2014 Max 280 characters. Should be punchy, curiosity-driven, and conversational. Include 2\u20133 relevant hashtags. May include a question or provocative statement to drive engagement. MUST be under 280 characters.\n\n2. **LinkedIn Post** \u2014 Professional tone, 150\u2013300 words. Lead with a strong insight or surprising finding from the thread. Use short paragraphs for readability. Include a clear lesson or takeaway for professionals. End with an engagement question. Include 3\u20135 relevant hashtags on the last line.\n\n\u26a0\ufe0f IMPORTANT: Return ONLY a raw JSON object. No markdown fences, no explanation, no preamble. Exactly this format:\n{\n  \"x_post\": \"your tweet text here\",\n  \"linkedin_post\": \"your linkedin post text here\"\n}"
            }
          ]
        },
        "builtInTools": {}
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "ce5e6e7c-41e3-405b-b771-28039757835e",
      "name": "Parse Social Posts",
      "type": "n8n-nodes-base.code",
      "position": [
        1600,
        32
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Parse Social Posts \u2014 handles Gemini's raw API response structure\n//\n// Gemini returns: array of candidates \u2192 content.parts[0].text\n// Shape: [ { content: { parts: [ { text: '...' } ], role: 'model' }, finishReason, index } ]\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst inputJson = $input.first().json;\n\n// \u2500\u2500 Extract raw text from wherever it lives \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet rawText = '';\n\n// Case 1: Gemini raw API response \u2014 array of candidates\nif (Array.isArray(inputJson) && inputJson[0]?.content?.parts?.[0]?.text) {\n  rawText = inputJson[0].content.parts[0].text;\n\n// Case 2: Single candidate object (not wrapped in array)\n} else if (inputJson?.content?.parts?.[0]?.text) {\n  rawText = inputJson.content.parts[0].text;\n\n// Case 3: LangChain chainLlm wrapper output \u2192 { text: '...' }\n} else if (typeof inputJson?.text === 'string') {\n  rawText = inputJson.text;\n\n// Case 4: Nested under candidates key\n} else if (inputJson?.candidates?.[0]?.content?.parts?.[0]?.text) {\n  rawText = inputJson.candidates[0].content.parts[0].text;\n\n} else {\n  throw new Error(\n    'Could not locate text in Gemini response. Received keys: ' +\n    JSON.stringify(Object.keys(inputJson))\n  );\n}\n\nrawText = rawText.trim();\n\n// \u2500\u2500 Strip markdown code fences if Gemini wrapped the JSON \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst cleaned = rawText\n  .replace(/^```json\\s*/i, '')\n  .replace(/^```\\s*/i, '')\n  .replace(/\\s*```$/i, '')\n  .trim();\n\n// \u2500\u2500 Parse JSON \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet parsed;\n\ntry {\n  parsed = JSON.parse(cleaned);\n} catch (e) {\n  // Fallback: regex extraction if JSON is slightly malformed\n  const xMatch  = cleaned.match(/\"x_post\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"/s);\n  const liMatch = cleaned.match(/\"linkedin_post\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"/s);\n\n  if (!xMatch || !liMatch) {\n    throw new Error(\n      'AI did not return valid JSON for social posts. Extracted text: ' +\n      rawText.substring(0, 400)\n    );\n  }\n\n  parsed = {\n    x_post:        xMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\\"/g, '\"'),\n    linkedin_post: liMatch[1].replace(/\\\\n/g, '\\n').replace(/\\\\\"/g, '\"')\n  };\n}\n\nif (!parsed.x_post || !parsed.linkedin_post) {\n  throw new Error(\n    'Parsed JSON is missing x_post or linkedin_post fields. Got: ' +\n    JSON.stringify(Object.keys(parsed))\n  );\n}\n\n// \u2500\u2500 Enforce X's 280-character limit with graceful truncation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet xPost = parsed.x_post.trim();\nif (xPost.length > 280) {\n  xPost = xPost.substring(0, 277) + '...';\n}\n\nconst linkedinPost = parsed.linkedin_post.trim();\n\nreturn [{\n  json: {\n    x_post:        xPost,\n    linkedin_post: linkedinPost,\n    x_char_count:  xPost.length\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "91133954-3028-45b8-a5a6-e07aab3db8ae",
      "name": "Resolve Final Posts",
      "type": "n8n-nodes-base.code",
      "position": [
        2192,
        32
      ],
      "parameters": {
        "jsCode": "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Resolve final post content\n//\n// After a Wait node resumes, $input only contains the FORM submission data.\n// The AI-generated posts from the earlier node are no longer in $input \u2014\n// we must reference that node explicitly by name.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// \u2500\u2500 Form submission data (what the user submitted) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst formData = $input.first().json;\n\n// \u2500\u2500 AI-generated posts from the upstream Parse node \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Reference the exact name of your Parse Social Posts code node\nconst aiData = $('Parse Social Posts').item.json;\n\nconst aiXPost        = (aiData.x_post        || '').trim();\nconst aiLinkedinPost = (aiData.linkedin_post  || '').trim();\n\n// \u2500\u2500 Override values typed by the user (empty string if left blank) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst overrideX  = (formData['X Post Override (leave blank to use AI version)']        || '').trim();\nconst overrideLi = (formData['LinkedIn Post Override (leave blank to use AI version)'] || '').trim();\n\n// \u2500\u2500 Final resolved posts: override wins, falls back to AI version \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst finalXPost        = overrideX  || aiXPost;\nconst finalLinkedinPost = overrideLi || aiLinkedinPost;\n\nif (!finalXPost) {\n  throw new Error('No X post content \u2014 AI version was empty and no override was provided.');\n}\nif (!finalLinkedinPost) {\n  throw new Error('No LinkedIn post content \u2014 AI version was empty and no override was provided.');\n}\n\nreturn [{\n  json: {\n    'X Post':        finalXPost,\n    'LinkedIn Post': finalLinkedinPost,\n    usedOverrideX:   !!overrideX,\n    usedOverrideLi:  !!overrideLi\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "a536ab57-8cef-4125-8cef-8b0143fbb9ea",
      "name": "Approved?",
      "type": "n8n-nodes-base.if",
      "position": [
        2480,
        32
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond-approval-check",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $('Human Approval').item.json['Decision'] }}",
              "rightValue": "Approve & Publish"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "bdf05261-fac9-476c-89cf-336396b2aaad",
      "name": "Post to Linkedin",
      "type": "n8n-nodes-base.linkedIn",
      "position": [
        2672,
        16
      ],
      "parameters": {
        "text": "={{ $json['LinkedIn Post'] }}",
        "person": "=ID",
        "additionalFields": {}
      },
      "credentials": {
        "linkedInOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "eb847f8f-8c5d-4a8b-b12a-77abdc5f4f43",
      "name": "Rejected \u2013 End",
      "type": "n8n-nodes-base.set",
      "position": [
        2672,
        208
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "set-rejected-status",
              "name": "status",
              "type": "string",
              "value": "rejected"
            },
            {
              "id": "set-rejected-msg",
              "name": "message",
              "type": "string",
              "value": "Posts were reviewed and rejected by the user. No content was published."
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "9f9afd49-2441-469b-b6de-2c6dce5f12ff",
      "name": "Post to X",
      "type": "n8n-nodes-base.twitter",
      "position": [
        2672,
        -176
      ],
      "parameters": {
        "text": "={{ $json['X Post'] }}",
        "additionalFields": {}
      },
      "credentials": {
        "twitterOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "d1a31113-4edd-46e0-8461-22df915e712a",
      "name": "\ud83d\uddd2\ufe0f Workflow Summary",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1040,
        -256
      ],
      "parameters": {
        "color": 4,
        "width": 604,
        "height": 532,
        "content": "## \ud83d\udd01 Reddit \u2192 Social Media Automation\n\nConverts any Reddit thread into ready-to-publish posts for **X (Twitter)** and **LinkedIn** \u2014 with a human review gate before anything goes live.\n\n\n---\n\n\n**\ud83d\udccc Data Flow:**\nReddit URL \u2192 Parse \u2192 Fetch API \u2192 Extract Content \u2192 AI Summarize \u2192 Generate Posts \u2192 Human Approval \u2192 Publish\n\n\n---\n\n\n**\u2728 Key Features:**\n- Accepts any Reddit thread URL\n- Extracts post + all nested comments (sorted by score)\n- AI-powered thread summarization via Gemini\n- Platform-optimized X + LinkedIn post generation\n- Human-in-the-loop approval with override support\n- Graceful rejection path \u2014 nothing published without approval\n\n---\n\n**\ud83d\udee0\ufe0f Tools Used:**\nGoogle Gemini 3.1 Flash Lite \u00b7 Reddit JSON API \u00b7 X (Twitter) API \u00b7 LinkedIn API"
      },
      "typeVersion": 1
    },
    {
      "id": "ce07e2c1-3244-40fb-8313-5cd50760b059",
      "name": "\ud83c\udf10 Node: Fetch Reddit Thread",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        -256
      ],
      "parameters": {
        "color": 2,
        "width": 284,
        "height": 528,
        "content": "## \ud83c\udf10 Fetch Reddit Thread\n**Type:** HTTP Request\n\n**Purpose:** Retrieves the full thread via Reddit's public JSON API.\n\n**Input:** `apiUrl` (e.g. `reddit.com/r/sub/comments/id.json`)\n\n**Function:** GET with `limit=100`, `depth=3`.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Output:** Raw Reddit API response \u2014 post listing + comments listing array."
      },
      "typeVersion": 1
    },
    {
      "id": "fb8c1a4a-84ca-4fc6-9201-7c4e97cb547d",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -416,
        -256
      ],
      "parameters": {
        "color": 3,
        "width": 256,
        "height": 528,
        "content": "## \ud83d\udce5 Reddit URL Input\n**Type:** Form Trigger\n\n**Purpose:** Entry point \u2014 presents a web form to collect a Reddit thread URL from the user.\n\n**Input:** User submits a Reddit thread URL via the form.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Output:** Raw URL string forwarded to the parse step."
      },
      "typeVersion": 1
    },
    {
      "id": "8c65fe79-b1a0-4541-a1e8-ee780dcbe283",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        448,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 288,
        "height": 528,
        "content": "## \u2699\ufe0f Extract Content\n**Type:** Code (JavaScript)\n\n**Purpose:** Parses raw Reddit JSON into structured, AI-ready content.\n\n**Input:** Raw Reddit API response\n\n**Function:** Extracts post metadata; recursively traverses all comments sorted by score at every depth; builds a flat indented string for AI.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Output:** `threadContent` (AI prompt string), `structuredComments` (nested JSON), post metadata"
      },
      "typeVersion": 1
    },
    {
      "id": "55e37b0d-7ae1-435e-9dfa-2ef05f1d5af1",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        752,
        -256
      ],
      "parameters": {
        "color": 5,
        "width": 352,
        "height": 528,
        "content": "## \ud83e\udde0 Summarize Thread\n**Type:** Google Gemini AI\n\n**Purpose:** Produces a comprehensive markdown summary of the Reddit thread.\n\n**Input:** `threadContent` from Extract node\n\n**Function:** Gemini 3.1 Flash Lite \u2192 returns Thread Overview, Key Topics, Notable Insights, Community Sentiment, and Actionable Takeaways.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Output:** Structured markdown summary text"
      },
      "typeVersion": 1
    },
    {
      "id": "3bce0319-33eb-4efb-b421-efe355e3ccdd",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        -256
      ],
      "parameters": {
        "color": 5,
        "width": 368,
        "height": 528,
        "content": "## \u270d\ufe0f Generate Social Posts\n**Type:** Google Gemini AI\n\n**Purpose:** Creates platform-optimized posts for X and LinkedIn.\n\n**Input:** Thread summary from Summarize node\n\n**Function:** Gemini generates: X post (\u2264280 chars, punchy + hashtags) and LinkedIn post (150\u2013300 words, professional + hashtags). Returns strict JSON only.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Output:** `{ \"x_post\": \"...\", \"linkedin_post\": \"...\" }`"
      },
      "typeVersion": 1
    },
    {
      "id": "b3beeef6-1f35-4f8c-bd03-4c13b5cabf8c",
      "name": "Human Approval",
      "type": "n8n-nodes-base.wait",
      "position": [
        1904,
        32
      ],
      "parameters": {
        "resume": "form",
        "options": {},
        "formTitle": "\ud83d\udccb Review & Approve Social Media Posts",
        "formFields": {
          "values": [
            {
              "fieldType": "textarea",
              "fieldLabel": "X Post Override (leave blank to use AI version)"
            },
            {
              "fieldType": "textarea",
              "fieldLabel": "LinkedIn Post Override (leave blank to use AI version)"
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "Decision",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Approve & Publish"
                  },
                  {
                    "option": "Reject"
                  }
                ]
              },
              "requiredField": true
            }
          ]
        },
        "formDescription": "=## \ud83d\udc26 X (TWITTER) POST  \u00b7  {{ $json.x_char_count }}/280 chars\n\n{{ $json.x_post }}\n\n---\n\n## \ud83d\udcbc LINKEDIN POST\n\n{{ $json.linkedin_post }}\n\n---\n\nThe posts above are ready to publish. You can paste an edited version in the override fields below \u2014 **leave a field blank to publish the AI version as-is**."
      },
      "typeVersion": 1.1
    },
    {
      "id": "067b6314-8cd7-4b22-aeea-9ac90332c687",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1504,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 288,
        "height": 528,
        "content": "## \ud83e\uddf9 Parse Social Posts\n**Type:** Code (JavaScript)\n\n**Purpose:** Safely extracts and validates Gemini's JSON response.\n\n**Input:** Raw Gemini API response\n\n**Function:** Strips markdown fences, parses JSON, falls back to regex if malformed. Enforces 280-char hard limit on X post.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Output:** `x_post`, `linkedin_post`, `x_char_count`"
      },
      "typeVersion": 1
    },
    {
      "id": "4fe0405c-4b1d-4fde-9a35-4fd611d07095",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1808,
        -256
      ],
      "parameters": {
        "color": 3,
        "width": 272,
        "height": 528,
        "content": "## \ud83d\udc64 Human Approval\n**Type:** Wait (Form)\n\n**Purpose:** Pauses the workflow so a human can review posts.\n\n**Input:** `x_post`, `linkedin_post`, `x_char_count`\n\n**Function:** Displays posts in a form. User can overrides or use AI version. Must select **Approve & Publish** or **Reject**.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Output:** Form submission with `Decision` + optional override fields."
      },
      "typeVersion": 1
    },
    {
      "id": "d36354a5-bd50-4ed3-90bd-55e0a767d9d2",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2096,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 288,
        "height": 528,
        "content": "## \u2705 Resolve Final Posts\n**Type:** Code (JavaScript)\n\n**Purpose:** Merges human overrides with AI-generated posts.\n\n**Input:** Form data + AI posts (referenced from Parse Social Posts node)\n\n**Function:** Override wins if non-empty; otherwise falls back to the AI version. Throws if both are empty.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Output:** `X Post`, `LinkedIn Post` (final content ready to publish)"
      },
      "typeVersion": 1
    },
    {
      "id": "b3874ed4-93f9-4dee-af61-67b014f87a3d",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2400,
        -256
      ],
      "parameters": {
        "color": 2,
        "width": 256,
        "height": 528,
        "content": "## \ud83d\udd00 Approved?\n**Type:** IF Condition\n\n**Purpose:** Routes the workflow based on the human's decision.\n\n**Input:** `Decision` field from Human Approval form\n\n**True \u2192** Post to X + Post to LinkedIn\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**False \u2192** Rejected \u2013 End (nothing published)"
      },
      "typeVersion": 1
    },
    {
      "id": "caf3cccc-4cf9-48af-ab39-b97f61fe9ab3",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2784,
        -256
      ],
      "parameters": {
        "color": 6,
        "width": 384,
        "height": 208,
        "content": "## \ud83d\udc26 Post to X\n**Type:** X (Twitter)\n\n**Purpose:** Publishes the final post to X (Twitter).\n\n\n**Input:** `X Post` (\u2264280 chars) from Resolve Final Posts\n\n**Output:** Live tweet on the connected X account."
      },
      "typeVersion": 1
    },
    {
      "id": "551598a4-3eb2-44a7-b14c-5ac1ab8e097e",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2784,
        -32
      ],
      "parameters": {
        "color": 6,
        "width": 384,
        "height": 192,
        "content": "## \ud83d\udcbc Post to LinkedIn\n**Type:** LinkedIn\n\n**Purpose:** Publishes the final post to LinkedIn.\n\n**Input:** `LinkedIn Post` from Resolve Final Posts\n\n**Output:** Live post on the connected LinkedIn account."
      },
      "typeVersion": 1
    },
    {
      "id": "19a0bf37-09c1-4a61-b1c8-b6bd41e6d166",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2784,
        176
      ],
      "parameters": {
        "color": 6,
        "width": 384,
        "height": 176,
        "content": "## \ud83d\udeab Rejected \u2013 End\n**Type:** Set\n\n**Purpose:** Graceful termination \u2014 nothing is published.\n\n**Input:** Rejection path from Approved? node\n\n**Function:** Sets `status: 'rejected'` and a descriptive message.\n\n**Output:** Terminal state \u2014 workflow ends here."
      },
      "typeVersion": 1
    },
    {
      "id": "87f9573d-43dd-4bee-b744-efd26711ffd5",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 288,
        "height": 528,
        "content": "## \ud83d\udd0d Parse Reddit URL\n**Type:** Code (JavaScript)\n\n**Purpose:** Validates and deconstructs the submitted Reddit URL.\n\n**Input:** Raw URL string\n\n**Function:** Regex extraction of subreddit + post ID. Handles standard, mobile (`m.`) and short `redd.it` links.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n**Output:** `subreddit`, `postId`, `apiUrl`, `cleanThreadUrl`"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "6173be76-a99e-4a57-800c-510df745cce5",
  "connections": {
    "Approved?": {
      "main": [
        [
          {
            "node": "Post to X",
            "type": "main",
            "index": 0
          },
          {
            "node": "Post to Linkedin",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Rejected \u2013 End",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Human Approval": {
      "main": [
        [
          {
            "node": "Resolve Final Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Reddit URL": {
      "main": [
        [
          {
            "node": "Fetch Reddit Thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reddit URL Input": {
      "main": [
        [
          {
            "node": "Parse Reddit URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summarize Thread": {
      "main": [
        [
          {
            "node": "Generate Social Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Social Posts": {
      "main": [
        [
          {
            "node": "Human Approval",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Reddit Thread": {
      "main": [
        [
          {
            "node": "Extract Thread Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve Final Posts": {
      "main": [
        [
          {
            "node": "Approved?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Social Posts": {
      "main": [
        [
          {
            "node": "Parse Social Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Thread Content": {
      "main": [
        [
          {
            "node": "Summarize Thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}