{
  "id": "zmRpMXO0azBftOGa",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "AI Hiring Signal Tracker powered by Bright Data",
  "tags": [],
  "nodes": [
    {
      "id": "21585fe6-d444-4383-9d58-6c1f19d51624",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2960,
        624
      ],
      "parameters": {
        "color": 7,
        "width": 540,
        "height": 820,
        "content": "# \ud83c\udfaf AI Hiring Signal Tracker powered by Bright Data\n\nTurn LinkedIn job postings into qualified pipeline. Detect when target companies are hiring sales/eng/leadership roles, score the signal with AI, draft personalized outreach, and sync to HubSpot + Slack.\n\n## How it works\n\n1. **Schedule Trigger** runs every morning\n2. **Airtable** returns the active LinkedIn job-search URLs to monitor\n3. **Bright Data Web Unlocker** scrapes each search results page\n4. **GPT extracts** structured job data from the raw HTML (title, company, location, etc.)\n5. **GPT scores** each job as a HIGH / MEDIUM / LOW buying signal and drafts a cold outreach email\n6. **HubSpot** creates a deal for HIGH/MEDIUM signals\n7. **Slack** alerts the sales team with the rationale and suggested email\n\n## Why Bright Data?\n\n- Scrapes LinkedIn search pages reliably (handles JS, anti-bot, geo)\n- Returns clean Markdown ready for LLM parsing\n- Scales from 1 to thousands of searches per day\n\n## Prerequisites\n\n- A [Bright Data](https://brightdata.com) account with a Web Unlocker zone\n- An [OpenAI](https://platform.openai.com) API key\n- An [Airtable](https://airtable.com) base with `Searches` table (`LinkedIn URL`, `Active`, `Last Run`)\n- A [HubSpot](https://hubspot.com) account\n- A [Slack](https://slack.com) workspace\n\n## Setup (~10 min)\n\n1. Connect Airtable, Bright Data, OpenAI, HubSpot and Slack credentials\n2. Add LinkedIn job-search URLs to Airtable with `Active = TRUE`\n3. Pick your Slack channel in the notification node\n4. Activate the workflow"
      },
      "typeVersion": 1
    },
    {
      "id": "de30580e-b7ad-4f20-90f8-3f2eabb33c39",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3680,
        1072
      ],
      "parameters": {
        "color": 4,
        "width": 620,
        "height": 376,
        "content": "## \ud83d\ude80 1. Trigger & Get Searches\n\nThe **Schedule Trigger** kicks off daily at 9am. **Airtable** returns the active LinkedIn search URLs (sorted by `Last Run` so the oldest gets refreshed first), and the **loop** processes them one by one.\n\n### \ud83d\udd04 Swap-friendly\n- Trigger \u2192 Webhook, Cron, Form, Chat\n- CRM \u2192 HubSpot, Notion, Sheets, Postgres\u2026"
      },
      "typeVersion": 1
    },
    {
      "id": "5032ad49-6baf-4cde-b007-19410673f52e",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4368,
        992
      ],
      "parameters": {
        "color": 3,
        "width": 720,
        "height": 460,
        "content": "## \ud83c\udf10 2. Scrape & Parse Jobs\n\n**Bright Data Web Unlocker** fetches the LinkedIn search page as clean Markdown. **GPT-5.4-mini** then extracts a structured list of up to 30 jobs (title, company, location, posted date, URL, early applicant flag\u2026). The **JS code** safely parses the JSON, even when the model truncates its output.\n\n- Bright Data handles JS, CAPTCHAs, geo-blocks\n- The LLM does the structured parsing \u2014 no fragile CSS selectors\n- \ud83d\udd04 Swap GPT for any LLM (Claude, Gemini, Mistral, Groq\u2026)"
      },
      "typeVersion": 1
    },
    {
      "id": "f8983555-290b-4351-8d18-2a84a4bc15b8",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5168,
        1008
      ],
      "parameters": {
        "color": 5,
        "width": 300,
        "height": 440,
        "content": "## \ud83e\udde0 3. AI Signal Analysis\n\nFor each job, **GPT-4o-mini** returns:\n- `signal_strength` (HIGH / MEDIUM / LOW)\n- `rationale` (1 sentence)\n- `outreach_email` (3-4 sentence draft)\n\nUses a strict JSON schema so the output is always valid.\n\n\ud83d\udd04 Swap for any LLM."
      },
      "typeVersion": 1
    },
    {
      "id": "77c208cc-eee7-445f-a57d-67e6ac143224",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5552,
        992
      ],
      "parameters": {
        "color": 6,
        "width": 720,
        "height": 696,
        "content": "## \ud83c\udfaf 4. Filter, Sync & Notify\n\n1. **IF** keeps only HIGH or MEDIUM signals (LOW signals are skipped)\n2. **HubSpot** creates a deal with the rationale and the suggested email\n3. **Slack** alerts the sales team with the full context and the HubSpot deal link\n\n### \ud83d\udd04 Swap-friendly\n- CRM \u2192 Salesforce, Pipedrive, Attio, Close, Notion\u2026\n- Channel \u2192 Email, Discord, Teams, Telegram, SMS\u2026"
      },
      "typeVersion": 1
    },
    {
      "id": "326c692a-e135-4fc3-aebf-671f260caf24",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        3728,
        1296
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "637868ad-9865-48a8-86f9-7cef8c3266c8",
      "name": "Get Active Searches",
      "type": "n8n-nodes-base.airtable",
      "position": [
        3936,
        1296
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_AIRTABLE_BASE_ID",
          "cachedResultUrl": "",
          "cachedResultName": "Hiring Signal Tracker"
        },
        "sort": {
          "property": [
            {
              "field": "Last Run"
            }
          ]
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_AIRTABLE_TABLE_ID",
          "cachedResultUrl": "",
          "cachedResultName": "Searches"
        },
        "options": {},
        "operation": "search",
        "filterByFormula": "{Active} = TRUE()"
      },
      "typeVersion": 2.2
    },
    {
      "id": "624a87ac-880f-4704-9504-2528a3f6d52f",
      "name": "Loop Over Searches",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        4144,
        1296
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "eab6ebf8-2470-4bec-82a5-d5e973d9cfd9",
      "name": "Scrape LinkedIn with Bright Data",
      "type": "@brightdata/n8n-nodes-brightdata.brightData",
      "position": [
        4432,
        1312
      ],
      "parameters": {
        "url": "={{ $json.fields['LinkedIn URL'] }}",
        "zone": {
          "__rl": true,
          "mode": "list",
          "value": "n8n_unlocker"
        },
        "country": {
          "__rl": true,
          "mode": "list",
          "value": "us"
        },
        "data_format": "markdown",
        "requestOptions": {}
      },
      "typeVersion": 1
    },
    {
      "id": "cb20e4bf-d04b-4613-8ce1-ea2e18cf24bc",
      "name": "Extract Jobs (AI)",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        4624,
        1312
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5.4-mini",
          "cachedResultName": "GPT-5.4-MINI"
        },
        "options": {},
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "You are a LinkedIn job posting parser. Extract structured data from raw LinkedIn jobs HTML.\n\nOutput ONLY valid JSON, no markdown, no preamble, no explanations.\n\nFormat:\n{\n  \"jobs\": [\n    {\n      \"title\": \"Account Executive\",\n      \"company\": \"Acme Corp\",\n      \"company_linkedin_slug\": \"acme-corp\",\n      \"location\": \"San Francisco, CA\",\n      \"posted_date\": \"2 hours ago\",\n      \"posted_iso\": \"2026-05-05\",\n      \"job_url\": \"https://www.linkedin.com/jobs/view/123456\",\n      \"early_applicant\": true,\n      \"actively_hiring\": true\n    }\n  ]\n}\n\nExtract from <li><div class=\"base-card ...\">...</div></li> blocks.\n- title: <h3 class=\"base-search-card__title\">\n- company: <h4 class=\"base-search-card__subtitle\"><a>...</a>\n- company_linkedin_slug: from the company link href, extract slug after /company/\n- location: <span class=\"job-search-card__location\">\n- posted_date: text inside <time>\n- posted_iso: from <time datetime=\"YYYY-MM-DD\">\n- job_url: from <a class=\"base-card__full-link\" href=\"...\">\n- early_applicant: true if \"Be an early applicant\" appears in the card\n- actively_hiring: true if \"Actively Hiring\" appears in the card\n\nLimit to first 30 jobs. If no jobs found, return {\"jobs\": []}."
            },
            {
              "content": "=Extract jobs from this LinkedIn HTML chunk:\n\n{{ $json }}"
            }
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2.1
    },
    {
      "id": "abc96954-7c82-4f96-ade8-2b63b7bac20b",
      "name": "Parse JSON Output",
      "type": "n8n-nodes-base.code",
      "position": [
        4976,
        1312
      ],
      "parameters": {
        "jsCode": "let text = $input.item.json.output[0].content[0].text;\n\n// Strip markdown backticks if present\ntext = text.replace(/```json\\s*/g, '').replace(/```\\s*/g, '').trim();\n\n// Try normal parse first\ntry {\n  const parsed = JSON.parse(text);\n  return { json: parsed };\n} catch (err) {\n  // If parse fails, try to recover what we can\n  // by finding the last \"}\" that closes a complete job object\n  const jobsStart = text.indexOf('\"jobs\":[');\n  if (jobsStart === -1) {\n    throw new Error('No jobs array found in response');\n  }\n  \n  let lastValidEnd = -1;\n  let depth = 0;\n  let inString = false;\n  let escape = false;\n  \n  for (let i = jobsStart + 8; i < text.length; i++) {\n    const c = text[i];\n    if (escape) { escape = false; continue; }\n    if (c === '\\\\') { escape = true; continue; }\n    if (c === '\"') { inString = !inString; continue; }\n    if (inString) continue;\n    if (c === '{') depth++;\n    if (c === '}') {\n      depth--;\n      if (depth === 0) lastValidEnd = i;\n    }\n  }\n  \n  if (lastValidEnd === -1) {\n    throw new Error('Could not recover any complete jobs');\n  }\n  \n  // Rebuild a valid truncated JSON\n  const truncated = text.substring(0, lastValidEnd + 1) + ']}';\n  const parsed = JSON.parse(truncated);\n  return { json: parsed };\n}"
      },
      "typeVersion": 2
    },
    {
      "id": "448dd4f2-76b6-4c22-a2f3-bcc4562bc97d",
      "name": "Score Signal & Draft Email",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        5200,
        1312
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "GPT-4O-MINI"
        },
        "options": {
          "textFormat": {
            "textOptions": {
              "type": "json_schema",
              "schema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"signal_strength\": {\n      \"type\": \"string\",\n      \"enum\": [\"HIGH\", \"MEDIUM\", \"LOW\"]\n    },\n    \"rationale\": {\n      \"type\": \"string\"\n    },\n    \"outreach_email\": {\n      \"type\": \"string\"\n    }\n  },\n  \"additionalProperties\": false,\n  \"required\": [\"signal_strength\", \"rationale\", \"outreach_email\"]\n}"
            }
          }
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "You are a B2B sales buying signal analyzer + outreach copywriter.\n\nGiven a job posting, classify if it's a buying signal AND draft a personalized cold outreach email if relevant.\n\nOutput ONLY valid JSON with this exact structure:\n\n{\n  \"signal_strength\": \"HIGH\" | \"MEDIUM\" | \"LOW\",\n  \"rationale\": \"1 sentence explaining why\",\n  \"outreach_email\": \"3-4 sentence cold email draft (only if HIGH or MEDIUM, otherwise empty string)\"\n}\n\nHIGH signals: Senior leadership hire (Head of, VP, Founding role), multiple roles open at same company, net-new sales hire at B2B SaaS.\n\nMEDIUM signals: Mid-level B2B sales/eng/marketing roles at growing companies.\n\nLOW signals: Junior IC roles, sports teams, retail/hospitality, agency staffing, geographic-specific sales (Tampa AE, etc.). Set outreach_email to \"\" for LOW signals.\n\nEmail style: 3-4 sentences max. Reference the specific hiring signal. Soft CTA (book 15 min). No \"I hope this finds you well\". First-person from the rep."
            },
            {
              "content": "=Title: {{ $json.jobs[0].title }}\nCompany: {{ $json.jobs[0].company }}\nLocation: {{ $json.jobs[0].location }}\nPosted: {{ $json.jobs[0].posted_date }}\nURL: {{ $json.jobs[0].job_url }}\n\nAnalyze and respond with JSON."
            }
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2.1
    },
    {
      "id": "cb0f575c-95eb-46c3-bb77-364f1031a4f6",
      "name": "HIGH or MEDIUM Signal?",
      "type": "n8n-nodes-base.if",
      "position": [
        5568,
        1312
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "2bd376ae-3438-41c7-8b8b-cc6fa7bf36a5",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.output[0].content[0].text.signal_strength }}",
              "rightValue": "HIGH"
            },
            {
              "id": "bc249f13-0a20-4e22-9cfc-a671095202f4",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.output[0].content[0].text.signal_strength }}",
              "rightValue": "MEDIUM"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "295f54c4-8e0a-40cb-9439-79e81bf315d6",
      "name": "Create HubSpot Deal",
      "type": "n8n-nodes-base.hubspot",
      "position": [
        5856,
        1408
      ],
      "parameters": {
        "stage": "appointmentscheduled",
        "resource": "deal",
        "authentication": "oAuth2",
        "additionalFields": {
          "dealName": "=Hiring signal \u2014 {{ $('Parse JSON Output').item.json.jobs[0].company }} hiring {{ $('Parse JSON Output').item.json.jobs[0].title }}",
          "description": "={{ $json.output[0].content[0].text.rationale }}\n\nSuggested email:\n{{ $json.output[0].content[0].text.outreach_email }}\n\nJob: {{ $('Parse JSON Output').item.json.jobs[0].title }}\nLocation: {{ $('Parse JSON Output').item.json.jobs[0].location }}"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "a3c3a68d-7e41-471c-baf1-139f98563d17",
      "name": "Notify Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        6128,
        1488
      ],
      "parameters": {
        "text": "=\ud83c\udfaf *New buying signal*\n\n\ud83d\udccb *{{ $('Parse JSON Output').item.json.jobs[0].title }}* at *{{ $('Parse JSON Output').item.json.jobs[0].company }}*\n\ud83d\udccd {{ $('Parse JSON Output').item.json.jobs[0].location }}\n\ud83d\udd25 Signal: *{{ $('Score Signal & Draft Email').item.json.output[0].content[0].text.signal_strength }}*\n\n\ud83d\udca1 *Why this matters:*\n{{ $('Score Signal & Draft Email').item.json.output[0].content[0].text.rationale }}\n\n\ud83d\udce7 *Suggested outreach:*\n{{ $('Score Signal & Draft Email').item.json.output[0].content[0].text.outreach_email }}\n\n\ud83d\udd17 <{{ $('Parse JSON Output').item.json.jobs[0].job_url }}|View job posting>\n\ud83d\udcbc HubSpot deal ID: {{ $json.dealId }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SLACK_CHANNEL_ID",
          "cachedResultName": "leads"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.4
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "b34ed4df-956c-4e29-82c4-38dbc8014235",
  "connections": {
    "Notify Slack": {
      "main": [
        [
          {
            "node": "Loop Over Searches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Get Active Searches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Jobs (AI)": {
      "main": [
        [
          {
            "node": "Parse JSON Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse JSON Output": {
      "main": [
        [
          {
            "node": "Score Signal & Draft Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Searches": {
      "main": [
        [],
        [
          {
            "node": "Scrape LinkedIn with Bright Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create HubSpot Deal": {
      "main": [
        [
          {
            "node": "Notify Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Active Searches": {
      "main": [
        [
          {
            "node": "Loop Over Searches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HIGH or MEDIUM Signal?": {
      "main": [
        [
          {
            "node": "Create HubSpot Deal",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Loop Over Searches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Signal & Draft Email": {
      "main": [
        [
          {
            "node": "HIGH or MEDIUM Signal?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape LinkedIn with Bright Data": {
      "main": [
        [
          {
            "node": "Extract Jobs (AI)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}