{
  "id": "mmmwxbhqMNBbYGXN",
  "name": "Workflow 4 - Job Board Aggregator",
  "tags": [],
  "nodes": [
    {
      "id": "c1e723cb-3a63-4806-97b9-2b6b198271b6",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -976,
        16
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 8
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "2a14399a-6aaa-48a5-b273-e4626edd4160",
      "name": "Set Search Variables",
      "type": "n8n-nodes-base.set",
      "position": [
        -752,
        16
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "kw1",
              "name": "keyword",
              "type": "string",
              "value": "Digital Marketing Manager"
            },
            {
              "id": "loc1",
              "name": "location",
              "type": "string",
              "value": "Lahore, Pakistan"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "37cb6fac-27e3-432b-90a5-b0b7e1a00205",
      "name": "Build Search URLs",
      "type": "n8n-nodes-base.code",
      "position": [
        -544,
        16
      ],
      "parameters": {
        "jsCode": "const keyword = encodeURIComponent($json.keyword);\nconst location = encodeURIComponent($json.location);\n\nconst linkedinUrl = `https://www.linkedin.com/jobs/search/?keywords=${keyword}&location=${location}`;\nconst indeedUrl = `https://www.indeed.com/jobs?q=${keyword}&l=${location}`;\n\nreturn [\n  { json: { source: 'linkedin', url: linkedinUrl, keyword: $json.keyword, location: $json.location } },\n  { json: { source: 'indeed', url: indeedUrl, keyword: $json.keyword, location: $json.location } }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "51b8d0b1-58ee-4390-875d-e7d4bf128b99",
      "name": "IF Source = LinkedIn",
      "type": "n8n-nodes-base.if",
      "position": [
        -320,
        16
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "cond1",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.source }}",
              "rightValue": "linkedin"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "737fa0e0-41b1-4d66-95a6-985fb3883156",
      "name": "ScrapeUnblocker",
      "type": "n8n-nodes-scrapeunblocker.scrapeUnblocker",
      "position": [
        0,
        0
      ],
      "parameters": {
        "url": "={{ $json.url }}"
      },
      "credentials": {},
      "typeVersion": 1
    },
    {
      "id": "ed9c6cfc-5bb6-438a-ba03-15fa04c686d9",
      "name": "Code in JavaScript",
      "type": "n8n-nodes-base.code",
      "position": [
        208,
        0
      ],
      "parameters": {
        "jsCode": "// Robustly find the HTML string wherever it lives in the input item\nfunction findHtml(value) {\n  if (typeof value === 'string' && value.includes('<html')) {\n    return value;\n  }\n  if (Array.isArray(value)) {\n    for (const v of value) {\n      const found = findHtml(v);\n      if (found) return found;\n    }\n  }\n  if (value && typeof value === 'object') {\n    for (const key of Object.keys(value)) {\n      const found = findHtml(value[key]);\n      if (found) return found;\n    }\n  }\n  return null;\n}\n\nconst html = findHtml($json);\n\nif (!html) {\n  throw new Error('No HTML found in input. Input structure: ' + JSON.stringify($json).slice(0, 200));\n}\n\nconst jobs = [];\n\nconst cardRegex = /<div[^>]*data-entity-urn=\"urn:li:jobPosting:(\\d+)\"[\\s\\S]*?<\\/div>\\s*<\\/li>/g;\nlet cardMatch;\n\nwhile ((cardMatch = cardRegex.exec(html)) !== null) {\n  const block = cardMatch[0];\n  const jobId = cardMatch[1];\n\n  const urlMatch = block.match(/class=\"base-card__full-link[^\"]*\"[^>]*href=\"([^\"]+)\"/);\n  const titleMatch = block.match(/class=\"base-search-card__title\">\\s*([^<]+)/);\n  const companyMatch = block.match(/class=\"base-search-card__subtitle\">[\\s\\S]*?<a[^>]*>\\s*([^<]+)/);\n  const locationMatch = block.match(/class=\"job-search-card__location\">\\s*([^<]+)/);\n  const dateMatch = block.match(/class=\"job-search-card__listdate\"[^>]*datetime=\"([^\"]+)\"/);\n\n  jobs.push({\n    jobId: jobId,\n    title: titleMatch ? titleMatch[1].trim() : null,\n    company: companyMatch ? companyMatch[1].trim() : null,\n    location: locationMatch ? locationMatch[1].trim() : null,\n    postDate: dateMatch ? dateMatch[1].trim() : null,\n    url: urlMatch ? urlMatch[1].replace(/&amp;/g, '&').split('?')[0] : null,\n    source: 'LinkedIn',\n    dateScraped: new Date().toISOString()\n  });\n}\n\nconst seen = new Set();\nconst deduped = jobs.filter(j => {\n  if (seen.has(j.jobId)) return false;\n  seen.add(j.jobId);\n  return true;\n});\n\nreturn deduped.map(job => ({ json: job }));"
      },
      "typeVersion": 2
    },
    {
      "id": "8a1f4805-3e44-44bd-8e09-d94a002835c5",
      "name": "Append row in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        416,
        0
      ],
      "parameters": {
        "columns": {
          "value": {
            "URL": "={{ $json.url }}",
            "Title": "={{ $json.title }}",
            "Job ID": "={{ $json.jobId }}",
            "Source": "={{ $json.source }}",
            "Company": "={{ $json.company }}",
            "Location": "={{ $json.location }}",
            "Post Date": "={{ $json.postDate }}",
            "Date Scraped": "={{ $json.dateScraped }}"
          },
          "schema": [
            {
              "id": "Job ID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Job ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Title",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Company",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Company",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Location",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Location",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Post Date",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Post Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Source",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "URL",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date Scraped",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date Scraped",
              "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/1K1CvGzVGOGpSfY6rAknO3XCyZ-hqr6KYH4ZKkiZpySM/edit#gid=0",
          "cachedResultName": "jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1K1CvGzVGOGpSfY6rAknO3XCyZ-hqr6KYH4ZKkiZpySM",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1K1CvGzVGOGpSfY6rAknO3XCyZ-hqr6KYH4ZKkiZpySM/edit?usp=drivesdk",
          "cachedResultName": "Job Board Aggregator"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "c13b1a67-2ce5-411b-a321-da392de89e7c",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1728,
        -304
      ],
      "parameters": {
        "width": 592,
        "height": 720,
        "content": "## Automated LinkedIn Job Board Scraper\n\n### How It Works\n* **Schedule & Input:** Runs every 8 hours, taking target search parameters (`keyword` & `location`).\n* **URL Building & Filtering:** Generates encoded search URLs for job platforms (LinkedIn/Indeed) and routes LinkedIn searches downstream via an IF condition.\n* **Scraping:** Fetches search results HTML using **ScrapeUnblocker** to bypass anti-bot protections.\n\n\n### Quick Setup Checklist\n1. **ScrapeUnblocker:** Authenticate active credentials.\n2. **Google Sheets:** Link credentials and verify target spreadsheet (`Job Board Aggregator`), sheet tab (`jobs`), and column mapping.\n\n### Customization\n* **Job Board Routing:** Expand `Build Search URLs` and add branches to `IF Source = LinkedIn` to scrape Indeed or other platforms.\n* **Regex Parsing:** Update card regex patterns inside `Code in JavaScript` if LinkedIn alters card containers or `data-entity-urn` tags."
      },
      "typeVersion": 1
    },
    {
      "id": "99db11e9-00fd-4233-aa99-7e0ffeecb6b0",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1056,
        -176
      ],
      "parameters": {
        "color": 7,
        "width": 656,
        "height": 384,
        "content": "## 1. Schedule Trigger & Search URL Generation\nTriggers the workflow every 8 hours, sets target search variables, builds encoded URLs for job sites, and routes LinkedIn tasks."
      },
      "typeVersion": 1
    },
    {
      "id": "30440fee-7ac7-4444-80d8-c1f66e2c3d7a",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -368,
        -176
      ],
      "parameters": {
        "color": 7,
        "width": 976,
        "height": 384,
        "content": "## 2. Scraping, Regex Parsing & Google Sheets Export\nFetches target page HTML via ScrapeUnblocker, parses job card details, deduplicates by Job ID, and appends listings to Google Sheets."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "15eb6764-3079-4cd3-9cc4-5d30108947dd",
  "nodeGroups": [],
  "connections": {
    "ScrapeUnblocker": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Set Search Variables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Search URLs": {
      "main": [
        [
          {
            "node": "IF Source = LinkedIn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Append row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Source = LinkedIn": {
      "main": [
        [
          {
            "node": "ScrapeUnblocker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Search Variables": {
      "main": [
        [
          {
            "node": "Build Search URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}