{
  "id": "Vt9g1PaZtILHlmma",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "LinkedIn Post Scraping & Lead Intent Scorer",
  "tags": [],
  "nodes": [
    {
      "id": "9b77dd56-68b5-418c-a142-a3537374c6c7",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -528,
        1040
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "4ad7e46d-e4fd-4646-be54-70a2038a0c1a",
      "name": "Main Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1104,
        768
      ],
      "parameters": {
        "color": 4,
        "width": 480,
        "height": 824,
        "content": "### How it works\n\nThis workflow scrapes LinkedIn posts that signal lead-generation pain points, extracts every comment on those posts, scores each commenter for buying intent using AI, and saves the ranked leads to a Google Sheet for follow-up.\n\n**Data flow:**\n1. A Google search (via SerpAPI) finds LinkedIn posts matching pain-point keywords such as \"missed leads\" or \"low conversion rate\".\n2. Raw search results are parsed and filtered to keep only valid LinkedIn post URLs.\n3. ConnectSafely fetches all comments on each post.\n4. Comments are flattened into individual rows.\n5. An AI agent (Azure OpenAI GPT-4o-mini) scores each comment on a 0\u2013100 intent scale.\n6. Parsed intent scores are appended to a Google Sheet.\n\n### Setup steps\n\n1. **SerpAPI** \u2013 Add your SerpAPI credential in the \"Search LinkedIn Posts\" node.\n2. **ConnectSafely** \u2013 Configure your ConnectSafely account credential (account ID required).\n3. **Azure OpenAI** \u2013 Add an Azure OpenAI credential with access to the gpt-4o-mini model.\n4. **Google Sheets** \u2013 Ensure the target sheet exists with columns: post_url, lead name, comment, intent score, intent label.\n\n### Customization\n\n- Edit the SerpAPI query keywords to target different pain points.\n- Adjust the intent-scoring prompt or thresholds inside the AI agent's system message.\n- Change the Google Sheet document ID or sheet name in the final node."
      },
      "typeVersion": 1
    },
    {
      "id": "26b9f48a-b5fc-417a-ba77-58d3a685be55",
      "name": "Section: Search & Scrape",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -560,
        880
      ],
      "parameters": {
        "color": 5,
        "width": 700,
        "height": 116,
        "content": "## Search & Scrape\n\nTriggers a Google search for pain-point posts on LinkedIn, parses the results, and fetches comments from each matched post via ConnectSafely."
      },
      "typeVersion": 1
    },
    {
      "id": "f014517f-3998-4c2b-baa0-496b1be32683",
      "name": "Section: Comment Processing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        192,
        816
      ],
      "parameters": {
        "color": 5,
        "width": 260,
        "height": 196,
        "content": "## Comment Processing\n\nReceives raw comment arrays from each post and flattens them into individual rows, one per commenter, ready for AI scoring."
      },
      "typeVersion": 1
    },
    {
      "id": "64cea62a-6624-49b0-a2f1-41329dd61953",
      "name": "Section: AI Intent Scoring",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        496,
        864
      ],
      "parameters": {
        "color": 5,
        "width": 600,
        "height": 100,
        "content": "## AI Intent Scoring\n\nEach comment is analysed by an AI agent that returns a 0\u2013100 intent score and a label (no-intent \u2192 high-intent). Output is parsed into clean rows."
      },
      "typeVersion": 1
    },
    {
      "id": "d3084ff1-14ff-4ad9-b601-bc923b41a772",
      "name": "Warning: SerpAPI",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -336,
        1248
      ],
      "parameters": {
        "color": 2,
        "width": 268,
        "height": 136,
        "content": "\u26a0\ufe0f **SerpAPI Key Required**\n\nThis node calls the Google Search API. A valid SerpAPI credential must be configured, or every execution will fail at this step."
      },
      "typeVersion": 1
    },
    {
      "id": "1a50334f-cfcb-41fe-8652-967df975a40e",
      "name": "Warning: ConnectSafely",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        1216
      ],
      "parameters": {
        "color": 2,
        "width": 252,
        "height": 152,
        "content": "\u26a0\ufe0f **ConnectSafely Credential Required**\n\nFetching post comments depends on an active ConnectSafely account. An invalid or expired API key will block the entire scrape."
      },
      "typeVersion": 1
    },
    {
      "id": "5be1f1fb-c185-4d54-a2aa-6fe5674bae5d",
      "name": "Warning: Azure OpenAI",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        576,
        1232
      ],
      "parameters": {
        "color": 2,
        "width": 268,
        "height": 152,
        "content": "\u26a0\ufe0f **Azure OpenAI Credential Required**\n\nThe AI intent agent uses gpt-4o-mini. Ensure the Azure deployment name matches and the credential is active; otherwise scoring will fail."
      },
      "typeVersion": 1
    },
    {
      "id": "1b825249-cc0d-46d5-9399-4b0a54e11f64",
      "name": "Search LinkedIn Posts via SerpAPI",
      "type": "n8n-nodes-serpapi.serpApi",
      "position": [
        -288,
        1040
      ],
      "parameters": {
        "q": "site:linkedin.com/posts (\"Missed leads\" OR \"Losing leads\" OR \"Low conversion rate\" OR \"Leads not converting\")",
        "requestOptions": {},
        "additionalFields": {}
      },
      "credentials": {
        "serpApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "8b562ae9-e9e4-4c5f-808f-6c627d9ac2b9",
      "name": "Fetch Post Comments via ConnectSafely",
      "type": "n8n-nodes-connectsafely-ai.connectSafelyLinkedIn",
      "position": [
        144,
        1040
      ],
      "parameters": {
        "postUrl": "={{ $json.post_url }}",
        "accountId": "695ce64a09c18d6bbbe90ed0",
        "operation": "getAllPostComments"
      },
      "credentials": {
        "connectSafelyApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "1d901be4-4abd-44ce-9c9d-7597381d84a8",
      "name": "Parse & Filter Search Results",
      "type": "n8n-nodes-base.code",
      "position": [
        -96,
        1040
      ],
      "parameters": {
        "jsCode": "// ---------------------------\n// COLLECT ALL POSSIBLE RESULTS\n// ---------------------------\nconst data = items[0].json;\n\nconst results = [\n  ...(data.organic_results || []),\n  ...(data.inline_results || []),\n  ...(data.discussions || []),\n  ...(data.forums || []),\n];\n\nconst problemKeywords = [\n  'problem',\n  'issue',\n  'manual',\n  'not working',\n  'struggling',\n  'pain',\n  'challenge',\n  'empty',\n  'ghosted',\n  'ghosting',\n  'not enough',\n  'low',\n  'hard',\n  'difficult',\n  'miss',\n  'failing'\n];\n\nconst output = results.map(post => {\n  const postUrl = post.link || '';\n  let platform = 'Unknown';\n  let username = null;\n  let postId = null;\n  let authorName = null;\n\n  // ---------------------------\n  // LINKEDIN DETECTION\n  // ---------------------------\n  if (/linkedin\\.com\\/posts\\//i.test(postUrl)) {\n    platform = 'LinkedIn';\n\n    const match = postUrl.match(/linkedin\\.com\\/posts\\/([^_]+).*?(\\d{8,})/i);\n    if (match) {\n      username = match[1];\n      postId = match[2];\n    }\n\n    authorName = post.source\n      ? post.source.replace('LinkedIn \u00b7 ', '').trim()\n      : null;\n  }\n\n  // ---------------------------\n  // REDDIT DETECTION (ROBUST)\n  // ---------------------------\n  if (/reddit\\.com/i.test(postUrl)) {\n    platform = 'Reddit';\n\n    const redditMatch = postUrl.match(\n      /reddit\\.com\\/r\\/([^/]+)\\/comments\\/([^/]+)/i\n    );\n\n    if (redditMatch) {\n      username = `r/${redditMatch[1]}`;\n      postId = redditMatch[2];\n    }\n\n    authorName = post.source\n      ? post.source.replace('Reddit \u00b7 ', '').trim()\n      : username;\n  }\n\n  // ---------------------------\n  // PROBLEM DETECTION\n  // ---------------------------\n  const text = `${post.title || ''} ${post.snippet || ''}`.toLowerCase();\n\n  const detectedProblems = problemKeywords.filter(k =>\n    text.includes(k)\n  );\n\n  return {\n    json: {\n      platform,\n      author_name: authorName,\n      username,\n      post_id: postId,\n      post_url: postUrl || null,\n      post_title: post.title || null,\n      post_snippet: post.snippet || null,\n      engagement_hint: post.displayed_link || null,\n      problem_keywords_detected: detectedProblems,\n      is_problem_post: detectedProblems.length > 0\n    }\n  };\n});\n\n// REMOVE EMPTY / UNKNOWN URL RESULTS\nreturn output.filter(item => item.json.post_url);\n\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a77130f6-5705-4267-9ca7-5a80e17b68da",
      "name": "AI Intent Detection Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        560,
        1040
      ],
      "parameters": {
        "text": "=Analyze the following LinkedIn comment and identify lead intent.\n\nPost URL:\n{{ $json.postUrl }}\n\nPost Content:\n{{ $json.postContent }}\n\nCommenter Name:\n{{ $json.commenterName }}\n\nComment Text:\n{{ $json.commentText }}\n\nReturn the result in the following JSON format ONLY:\n\n{\n  \"postUrl\": \"{{ $json.postUrl }}\",\n  \"leadName\": \"{{ $json.commenterName }}\",\n  \"comment\": \"{{ $json.commentText }}\",\n  \"intentScore\": number,\n  \"intentLabel\": \"no-intent | passive-interest | problem-aware | solution-aware | high-intent\"\n}\n",
        "options": {
          "systemMessage": "=You are a B2B sales intent detection agent.\n\nYour job is to analyze LinkedIn post context and comment text to identify whether the commenter shows buying intent, problem awareness, or decision-maker interest.\n\nYou must:\n- Use ONLY the data provided in the input\n- Infer intent from wording, tone, and relevance to the post\n- Be conservative: do not hallucinate intent\n- Prefer precision over recall\n\nScoring rules:\n- 0\u201320 \u2192 No intent (generic praise, agreement, emojis)\n- 21\u201340 \u2192 Passive interest (thoughtful comment, but no problem ownership)\n- 41\u201360 \u2192 Problem-aware (mentions pain points, challenges, or agreement with problem)\n- 61\u201380 \u2192 Solution-aware (implies need for solution, improvement, or optimization)\n- 81\u2013100 \u2192 High buying intent (clear ownership, decision role, desire to act)\n\nIf intent is below 30, still return the record but mark it as \"low-intent\".\n\nOutput MUST be valid JSON and follow the exact schema provided.\nDo not add explanations or extra text.\n"
        },
        "promptType": "define"
      },
      "typeVersion": 3
    },
    {
      "id": "9ccedfae-79a5-402a-85d3-554cee94fae4",
      "name": "Azure OpenAI GPT-4o-mini",
      "type": "@n8n/n8n-nodes-langchain.lmChatAzureOpenAi",
      "position": [
        432,
        1248
      ],
      "parameters": {
        "model": "gpt-4o-mini",
        "options": {}
      },
      "credentials": {
        "azureOpenAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "64643688-528f-456d-aaec-e19baff2137f",
      "name": "Flatten Comments into Rows",
      "type": "n8n-nodes-base.code",
      "position": [
        352,
        1040
      ],
      "parameters": {
        "jsCode": "const results = [];\n\nfor (const item of items) {\n  const post = item.json;\n\n  const postUrl = post.postUrl || '';\n  const postContent = post.postDetails?.content || '';\n  const activityUrn = post.postDetails?.activityUrn || null;\n\n  const comments = post.comments || [];\n\n  for (const comment of comments) {\n    results.push({\n      json: {\n        // Post info\n        postUrl,\n        activityUrn,\n        postContent,\n\n        // Comment info\n        commentId: comment.commentId,\n        commentText: comment.commentText,\n        commenterName: comment.authorName,\n        commenterProfileUrl: comment.profileUrl || null,\n        commenterUrn: comment.commenterUrn || null,\n\n        // Metadata\n        createdAt: comment.createdAt,\n        likeCount: comment.likeCount,\n        replyCount: comment.replyCount,\n        isProfile: comment.hasProfile\n      }\n    });\n  }\n}\n\nreturn results;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "27741bee-6896-460a-8bb2-333065225462",
      "name": "Parse AI Intent Output",
      "type": "n8n-nodes-base.code",
      "position": [
        912,
        1040
      ],
      "parameters": {
        "jsCode": "const results = [];\n\nfor (const item of items) {\n  // Skip empty or invalid outputs safely\n  if (!item.json.output) continue;\n\n  let parsed;\n  try {\n    parsed = JSON.parse(item.json.output);\n  } catch (e) {\n    // If parsing fails, skip this record\n    continue;\n  }\n\n  results.push({\n    json: {\n      postUrl: parsed.postUrl || null,\n      leadName: parsed.leadName || null,\n      comment: parsed.comment || null,\n      intentScore: parsed.intentScore ?? null,\n      intentLabel: parsed.intentLabel || null\n    }\n  });\n}\n\nreturn results;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "1d190ab7-11b2-40db-bc24-a7a2f16df196",
      "name": "Save Leads to Google Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1136,
        1040
      ],
      "parameters": {
        "columns": {
          "value": {
            "comment": "={{ $json.comment }}",
            "post_url": "={{ $json.postUrl }}",
            "lead name": "={{ $json.leadName }}",
            "intent label": "={{ $json.intentLabel }}",
            "intent score": "={{ $json.intentScore }}"
          },
          "schema": [
            {
              "id": "post_url",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "post_url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "lead name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "lead name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "comment",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "comment",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "intent score",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "intent score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "intent label",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "intent label",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1hXDNE90IFbXLgyLt2XwSHgI_9sASzTSM9LAA4cUDFj4/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1hXDNE90IFbXLgyLt2XwSHgI_9sASzTSM9LAA4cUDFj4",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1hXDNE90IFbXLgyLt2XwSHgI_9sASzTSM9LAA4cUDFj4/edit?usp=drivesdk",
          "cachedResultName": "Post Scraping"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "3c109119-fdf5-4216-92ef-7f00bbde7c2f",
  "connections": {
    "Parse AI Intent Output": {
      "main": [
        [
          {
            "node": "Save Leads to Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Azure OpenAI GPT-4o-mini": {
      "ai_languageModel": [
        [
          {
            "node": "AI Intent Detection Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "AI Intent Detection Agent": {
      "main": [
        [
          {
            "node": "Parse AI Intent Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flatten Comments into Rows": {
      "main": [
        [
          {
            "node": "AI Intent Detection Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse & Filter Search Results": {
      "main": [
        [
          {
            "node": "Fetch Post Comments via ConnectSafely",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search LinkedIn Posts via SerpAPI": {
      "main": [
        [
          {
            "node": "Parse & Filter Search Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "Search LinkedIn Posts via SerpAPI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Post Comments via ConnectSafely": {
      "main": [
        [
          {
            "node": "Flatten Comments into Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}