{
  "id": "zThgJDkFr6kU9SI2",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Talent Atlas AI",
  "tags": [],
  "nodes": [
    {
      "id": "2da6e92f-cbd8-4ada-8bb0-8d90dfc31263",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        14144,
        5776
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Talent Atlas Final\n\n### How it works\n\n1. The workflow receives a candidate search request via a webhook, validates and extracts job requirements using an AI agent.\n2. It builds a search brief and queries multiple sources (GitHub, LinkedIn via Apify, Stack Overflow, Hacker News) in parallel.\n3. Results from each source are normalized, merged, sanitized and deduplicated into a candidate pool.\n4. If candidates exist, the workflow scores evidence, builds a recruiter-facing shortlist, optionally saves candidates to Supabase, and returns a summarized result; if no candidates are found or the request is invalid it responds accordingly.\n5. AI is used twice: to extract JD requirements from the request and to summarize the final shortlist for recruiters.\n\n### Setup steps\n\n- [ ] Deploy the webhook endpoint and point your client to the Candidate Search Webhook URL.\n- [ ] Configure AI credentials (Groq/OpenAI) for the agent nodes (Extract JD Requirements, Summarize Shortlist).\n- [ ] Add API credentials: Apify key for LinkedIn scraping, GitHub token for user/repos endpoints, and Stack Exchange/HN access if applicable.\n- [ ] Configure Supabase credentials and database/table details if you want Save Candidate to Supabase enabled.\n- [ ] Ensure environment variables used by custom code nodes (rate limits, default search parameters) are set in n8n.\n\n### Customization\n\nAdjust search brief construction and normalization code to tune which candidate attributes are returned; change the summarization prompt or model settings to alter recruiter-facing output; enable/disable Supabase save with the Save Results Enabled? conditional."
      },
      "typeVersion": 1
    },
    {
      "id": "5533cdbe-8469-4ca5-b44e-b56d909c5e1d",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        14704,
        6496
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Receive and normalize request\n\nWebhook trigger and initial input normalization that prepares the incoming request payload for validation and downstream processing."
      },
      "typeVersion": 1
    },
    {
      "id": "e237fada-b7d0-4b68-9015-fdf945117cb4",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        15152,
        6528
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 464,
        "content": "## Validate request and errors\n\nChecks whether the incoming request is valid and, on failure, builds and returns a 400 Invalid Request response."
      },
      "typeVersion": 1
    },
    {
      "id": "37640bf9-0fc0-435a-959b-b1dcf9b82cad",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        15376,
        5984
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 512,
        "content": "## Extract JD and brief\n\nAI extraction of job description requirements and construction of the search brief used to query external sources."
      },
      "typeVersion": 1
    },
    {
      "id": "d281563a-a37a-45b9-937b-c3f0d6e55f31",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        15952,
        6240
      ],
      "parameters": {
        "color": 7,
        "width": 1536,
        "height": 336,
        "content": "## Search GitHub profiles\n\nQuery GitHub for users by role/location, split results, fetch repositories and profiles, group by owner, and normalize GitHub evidence items."
      },
      "typeVersion": 1
    },
    {
      "id": "ce973fa9-9b26-443f-9768-1862ce08d313",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        17072,
        5888
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Search LinkedIn via Apify\n\nCall Apify act to scrape LinkedIn results for the search brief and normalize the LinkedIn output into the pipeline format."
      },
      "typeVersion": 1
    },
    {
      "id": "f758a294-27f9-4a26-b4b5-f02003eca3ff",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        17520,
        6016
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 384,
        "content": "## Merge LinkedIn and GitHub\n\nAppend LinkedIn and GitHub normalized outputs together as a single stream for downstream merging."
      },
      "typeVersion": 1
    },
    {
      "id": "38429245-be94-4ba6-8ffb-185971e98426",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16400,
        6608
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 304,
        "content": "## Search Stack Overflow\n\nQuery Stack Exchange for expert users, split results and normalize them into the pipeline format."
      },
      "typeVersion": 1
    },
    {
      "id": "7fe8eb17-271e-4405-8783-ae7cf154c480",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        15952,
        6944
      ],
      "parameters": {
        "color": 7,
        "width": 1088,
        "height": 304,
        "content": "## Search Hacker News\n\nFind the latest hiring thread on Hacker News, select the exact thread, search candidate posts, split results and normalize items."
      },
      "typeVersion": 1
    },
    {
      "id": "b8077ff6-d254-430b-aaaf-2a7f65230b76",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        17072,
        6608
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 384,
        "content": "## Merge Stack Overflow and HN\n\nAppend and unify the normalized Stack Overflow and Hacker News outputs into a combined stream."
      },
      "typeVersion": 1
    },
    {
      "id": "b436c045-5bd1-4d93-bc49-ca8669b18f7b",
      "name": "Sticky Note10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        17792,
        6208
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 368,
        "content": "## Merge all sources\n\nCombine the LinkedIn/GitHub stream and the StackOverflow/HN stream into a single candidate pool for sanitation."
      },
      "typeVersion": 1
    },
    {
      "id": "f3d24fc9-1c25-4cda-880e-fe242c55aac1",
      "name": "Sticky Note11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        18064,
        6208
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 368,
        "content": "## Sanitize candidate pool\n\nRun deduplication, filtering and normalization across the combined candidate list to prepare for scoring."
      },
      "typeVersion": 1
    },
    {
      "id": "7f7ce570-963a-4afd-9261-f97e88b67f99",
      "name": "Sticky Note12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        18336,
        6288
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 496,
        "content": "## Handle no results\n\nIf no candidates remain after sanitation, build the no-results response and return a 200 with an empty result set."
      },
      "typeVersion": 1
    },
    {
      "id": "ae4f0f63-4441-42e8-a1e6-661085869fdc",
      "name": "Sticky Note13",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        18416,
        5776
      ],
      "parameters": {
        "color": 7,
        "width": 1440,
        "height": 480,
        "content": "## Score and summarize shortlist\n\nCalculate evidence scores for candidates, build the recruiter response, generate a summarized shortlist via AI, attach it and respond with results."
      },
      "typeVersion": 1
    },
    {
      "id": "5fc775f7-49d6-4dad-b7c9-bb7fb2302c75",
      "name": "Sticky Note14",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        18640,
        6288
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Save results to Supabase\n\nConditional branch that optionally persists candidate records to Supabase if saving is enabled."
      },
      "typeVersion": 1
    },
    {
      "id": "ce431579-8f65-4546-ba81-77ea1e3ab81f",
      "name": "Candidate Search Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        14752,
        6656
      ],
      "parameters": {
        "path": "talent-atlas-candidate-search",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "2700e68c-6b05-444a-b013-c402626b4c6e",
      "name": "Normalize Webhook Input",
      "type": "n8n-nodes-base.code",
      "position": [
        14976,
        6656
      ],
      "parameters": {
        "jsCode": "const incoming = $input.first().json;\nlet payload = incoming.body ?? incoming;\n\nif (typeof payload === 'string') {\n  try {\n    payload = JSON.parse(payload);\n  } catch {\n    payload = { job_description: payload };\n  }\n}\n\npayload = payload && typeof payload === 'object' ? payload : {};\n\nconst firstText = (...values) => {\n  for (const value of values) {\n    if (typeof value === 'string' && value.trim()) return value.trim();\n  }\n  return '';\n};\n\nconst toArray = (value) => {\n  if (Array.isArray(value)) {\n    return [...new Set(value.map((item) => String(item).trim()).filter(Boolean))];\n  }\n  if (typeof value !== 'string') return [];\n  return [...new Set(value.split(/[,\\n;|]/).map((item) => item.trim()).filter(Boolean))];\n};\n\nconst clamp = (value, fallback, min, max) => {\n  const number = Number(value);\n  return Number.isFinite(number) ? Math.min(max, Math.max(min, Math.round(number))) : fallback;\n};\n\nconst toBoolean = (value, fallback = false) => {\n  if (typeof value === 'boolean') return value;\n  if (typeof value === 'number') return value !== 0;\n  if (typeof value === 'string') {\n    const normalized = value.trim().toLowerCase();\n    if (['true', '1', 'yes', 'y'].includes(normalized)) return true;\n    if (['false', '0', 'no', 'n'].includes(normalized)) return false;\n  }\n  return fallback;\n};\n\nconst jobDescription = firstText(\n  payload.job_description,\n  payload.jobDescription,\n  payload.jd,\n  payload.description,\n);\n\nconst explicit = {\n  job_title: firstText(payload.job_title, payload.jobTitle, payload.title),\n  skills: toArray(payload.skills ?? payload.required_skills ?? payload.keywords),\n  location: firstText(payload.location, payload.job_location),\n  experience: firstText(payload.experience, payload.years_of_experience),\n  seniority: firstText(payload.seniority, payload.level),\n  employment_type: firstText(payload.employment_type, payload.employmentType),\n};\n\nconst valid = Boolean(jobDescription || explicit.job_title || explicit.skills.length);\nconst synthesizedDescription = [\n  explicit.job_title && 'Job title: ' + explicit.job_title,\n  explicit.skills.length && 'Required skills: ' + explicit.skills.join(', '),\n  explicit.location && 'Location: ' + explicit.location,\n  explicit.experience && 'Experience: ' + explicit.experience,\n  explicit.seniority && 'Seniority: ' + explicit.seniority,\n  explicit.employment_type && 'Employment type: ' + explicit.employment_type,\n].filter(Boolean).join('\\n');\n\nreturn [{\n  json: {\n    valid,\n    error: valid ? null : 'Provide job_description (or jd), or provide job_title/skills.',\n    request_id: firstText(payload.request_id, payload.requestId) ||\n      'req_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 8),\n    job_description: jobDescription || synthesizedDescription,\n    explicit,\n    max_candidates_per_source: clamp(\n      payload.max_candidates_per_source ?? payload.max_candidates,\n      5,\n      1,\n      10,\n    ),\n    max_results: clamp(payload.max_results, 20, 1, 50),\n    score_threshold: clamp(payload.score_threshold, 65, 0, 100),\n    save_results: toBoolean(payload.save_results, false),\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "509cb265-318e-45c2-9271-7147dfe2649f",
      "name": "Request Is Valid?",
      "type": "n8n-nodes-base.if",
      "position": [
        15200,
        6656
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f2f677a7-338d-4547-ae12-c96541484a76",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.valid }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "2c61d6f0-48a1-4250-94d7-6d07847e1529",
      "name": "Build Invalid Request Response",
      "type": "n8n-nodes-base.code",
      "position": [
        15488,
        6816
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\nreturn [{\n  json: {\n    ok: false,\n    error: input.error,\n    accepted_input: {\n      raw: ['job_description', 'jd', 'description'],\n      structured: ['job_title', 'skills', 'location', 'experience', 'seniority', 'employment_type'],\n      options: ['max_candidates_per_source', 'max_results', 'score_threshold', 'save_results'],\n    },\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "df12ff4a-ebcb-4483-8148-189334cda892",
      "name": "Respond 400 - Invalid Request",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        15776,
        6816
      ],
      "parameters": {
        "options": {
          "responseCode": 400
        },
        "respondWith": "json",
        "responseBody": "={{ $json }}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "95227dfe-aecd-4a78-a74f-40376aaeb360",
      "name": "Extract JD Requirements",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "maxTries": 2,
      "position": [
        15424,
        6112
      ],
      "parameters": {
        "text": "={{ 'Extract a structured hiring brief from this job description. Preserve explicit requirements and do not invent mandatory qualifications.\\n\\nJOB DESCRIPTION:\\n' + $json.job_description }}",
        "options": {
          "systemMessage": "You are a precise job-description parsing engine. Return only the structured object required by the output parser. Separate must-have skills from nice-to-have skills. Use empty strings or empty arrays when the JD does not provide a value."
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "retryOnFail": true,
      "typeVersion": 3.1,
      "waitBetweenTries": 1000
    },
    {
      "id": "32aadb45-a42f-49ff-80fe-f91a48ef265d",
      "name": "Model - Groq JD Extraction",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "position": [
        15440,
        6336
      ],
      "parameters": {
        "model": "llama-3.3-70b-versatile",
        "options": {
          "temperature": 0.1,
          "maxTokensToSample": 1000
        }
      },
      "typeVersion": 1
    },
    {
      "id": "788ff33d-95c2-4cc6-a9f4-f9812dfb87d3",
      "name": "JD Requirements Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        15568,
        6336
      ],
      "parameters": {
        "jsonSchemaExample": "{\"job_title\":\"Senior Backend Engineer\",\"location\":\"Remote\",\"experience\":\"5+ years\",\"seniority\":\"Senior\",\"employment_type\":\"Full-time\",\"skills\":[\"Node.js\",\"PostgreSQL\",\"AWS\"],\"must_have_skills\":[\"Node.js\",\"PostgreSQL\"],\"nice_to_have_skills\":[\"Kubernetes\"],\"responsibilities\":[\"Design and operate backend services\"],\"education\":\"\",\"keywords\":[\"backend\",\"distributed systems\",\"REST APIs\"]}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "4d9b09b6-23a8-452f-8a36-285a7adf166f",
      "name": "Build Search Brief",
      "type": "n8n-nodes-base.code",
      "position": [
        15776,
        6208
      ],
      "parameters": {
        "jsCode": "const normalized = $('Normalize Webhook Input').first().json;\nconst input = $input.first().json;\nconst extracted = input.output && typeof input.output === 'object' ? input.output : {};\nconst explicit = normalized.explicit || {};\n\nconst toArray = (value) => {\n  if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);\n  if (typeof value === 'string') return value.split(/[,\\n;|]/).map((item) => item.trim()).filter(Boolean);\n  return [];\n};\n\nconst unique = (values) => [...new Set(values.filter(Boolean))];\nconst skills = explicit.skills?.length\n  ? explicit.skills\n  : unique([\n      ...toArray(extracted.must_have_skills),\n      ...toArray(extracted.skills),\n      ...toArray(extracted.nice_to_have_skills),\n    ]);\n\nconst primarySkill = skills[0] || 'javascript';\nconst normalizedSkill = primarySkill.toLowerCase().replace(/\\s+/g, '');\n\nconst githubLanguageMap = {\n  '.net': 'C#',\n  '.netcore': 'C#',\n  'asp.net': 'C#',\n  'asp.netcore': 'C#',\n  'c#': 'C#',\n  'react': 'JavaScript',\n  'react.js': 'JavaScript',\n  'reactjs': 'JavaScript',\n  'node': 'JavaScript',\n  'node.js': 'JavaScript',\n  'nodejs': 'JavaScript',\n  'typescript': 'TypeScript',\n  'python': 'Python',\n  'java': 'Java',\n  'golang': 'Go',\n  'go': 'Go',\n  'ruby': 'Ruby',\n  'php': 'PHP',\n  'rust': 'Rust',\n};\n\nconst stackOverflowTagMap = {\n  '.net': '.net',\n  '.netcore': 'asp.net-core',\n  'asp.net': 'asp.net',\n  'asp.netcore': 'asp.net-core',\n  'c#': 'c#',\n  'react': 'reactjs',\n  'react.js': 'reactjs',\n  'reactjs': 'reactjs',\n  'node': 'node.js',\n  'node.js': 'node.js',\n  'nodejs': 'node.js',\n};\n\nconst githubTermMap = {\n  '.net': 'dotnet',\n  '.net core': 'asp.net-core',\n  '.netcore': 'asp.net-core',\n  'asp.net': 'asp.net-core',\n  'asp.net core': 'asp.net-core',\n  'asp.netcore': 'asp.net-core',\n  'c#': 'csharp',\n  'react': 'react',\n  'react.js': 'react',\n  'reactjs': 'react',\n  'typescript': 'typescript',\n  'rest api': 'rest-api',\n  'rest apis': 'rest-api',\n  'sql server': 'sql-server',\n  'azure': 'azure',\n  'docker': 'docker',\n  'microservices': 'microservices',\n  'kubernetes': 'kubernetes',\n  'node': 'nodejs',\n  'node.js': 'nodejs',\n  'nodejs': 'nodejs',\n};\n\nconst githubLanguage = githubLanguageMap[normalizedSkill] || primarySkill;\nconst githubTerms = unique(\n  skills.map((skill) =>\n    githubTermMap[String(skill).trim().toLowerCase()] ||\n    String(skill).trim().toLowerCase().replace(/\\s+/g, '-')\n  ),\n).filter((term) =>\n  !(githubLanguage.toLowerCase() === 'c#' && term === 'csharp')\n);\nconst repositoryTerms = githubTerms.slice(0, 2);\nconst pushedAfterYear = new Date().getUTCFullYear() - 2;\nconst resolvedJobTitle = explicit.job_title || extracted.job_title || 'Software Engineer';\nconst linkedinTitleVariants = unique([\n  resolvedJobTitle,\n  resolvedJobTitle.replace(/^(senior|lead|principal)\\s+/i, '').trim(),\n  resolvedJobTitle.replace(/\\bdeveloper\\b/i, 'Engineer'),\n  resolvedJobTitle.replace(/\\bengineer\\b/i, 'Developer'),\n]).filter(Boolean);\nconst githubRoleTerms = resolvedJobTitle\n  .toLowerCase()\n  .split(/[^a-z0-9+#.]+/)\n  .filter((term) =>\n    term.length >= 4 &&\n    !['senior', 'junior', 'lead', 'principal', 'developer', 'engineer'].includes(term)\n  )\n  .slice(0, 3)\n  .join(' ');\nconst githubLocationQualifier = normalized.explicit?.location || extracted.location\n  ? 'location:\"' + (normalized.explicit.location || extracted.location) + '\"'\n  : '';\n\nconst brief = {\n  request_id: normalized.request_id,\n  job_title: resolvedJobTitle,\n  location: explicit.location || extracted.location || 'Remote',\n  experience: explicit.experience || extracted.experience || '',\n  seniority: explicit.seniority || extracted.seniority || '',\n  employment_type: explicit.employment_type || extracted.employment_type || '',\n  skills: unique(skills),\n  skills_str: unique(skills).join(', '),\n  must_have_skills: unique(toArray(extracted.must_have_skills).length ? toArray(extracted.must_have_skills) : skills),\n  nice_to_have_skills: unique(toArray(extracted.nice_to_have_skills)),\n  responsibilities: unique(toArray(extracted.responsibilities)),\n  education: extracted.education || '',\n  keywords: unique([...toArray(extracted.keywords), ...skills]),\n  primary_skill: primarySkill,\n  linkedin_title_variants: linkedinTitleVariants,\n  linkedin_search_query: [\n    resolvedJobTitle,\n    ...unique(toArray(extracted.must_have_skills).length ? toArray(extracted.must_have_skills) : skills).slice(0, 4),\n  ].join(' '),\n  github_language: githubLanguage,\n  github_repository_terms: repositoryTerms,\n  github_user_query: [\n    githubRoleTerms,\n    'language:' + githubLanguage,\n    githubLocationQualifier,\n    'repos:>3',\n    'type:user',\n  ].filter(Boolean).join(' '),\n  github_repository_query: [\n    ...repositoryTerms,\n    'in:name,description,readme',\n    'language:' + githubLanguage,\n    'fork:false',\n    'archived:false',\n    'pushed:>' + pushedAfterYear + '-01-01',\n  ].join(' '),\n  stackoverflow_tag: stackOverflowTagMap[normalizedSkill] || primarySkill.toLowerCase().replace(/\\s+/g, '-'),\n  max_candidates_per_source: normalized.max_candidates_per_source,\n  max_results: normalized.max_results,\n  score_threshold: normalized.score_threshold,\n  save_results: normalized.save_results,\n};\n\nreturn [{ json: brief }];"
      },
      "typeVersion": 2
    },
    {
      "id": "197b82fd-4975-4a6f-a9d3-e121fe7771a9",
      "name": "Search LinkedIn via Apify",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        17120,
        6048
      ],
      "parameters": {
        "url": "https://api.apify.com/v2/acts/harvestapi~linkedin-profile-search/run-sync-get-dataset-items",
        "method": "POST",
        "options": {
          "timeout": 120000
        },
        "jsonBody": "={{ ({ profileScraperMode: 'Full', searchQuery: $json.linkedin_search_query, currentJobTitles: $json.linkedin_title_variants, locations: [$json.location], maxItems: $json.max_candidates_per_source, takePages: 1 }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBearerAuth"
      },
      "executeOnce": true,
      "retryOnFail": true,
      "typeVersion": 4.4,
      "alwaysOutputData": true,
      "waitBetweenTries": 2000
    },
    {
      "id": "df942017-0b85-42d2-98d7-2c5a32008740",
      "name": "Normalize LinkedIn",
      "type": "n8n-nodes-base.code",
      "position": [
        17344,
        6048
      ],
      "parameters": {
        "jsCode": "const scalarText = (value, preferredKeys = []) => {\n  if (typeof value === 'string') return value.trim();\n  if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n  if (!value || typeof value !== 'object') return '';\n\n  if (Array.isArray(value)) {\n    return value\n      .map((item) => scalarText(item, preferredKeys))\n      .filter(Boolean)\n      .join(', ');\n  }\n\n  const commonKeys = [\n    ...preferredKeys,\n    'name',\n    'title',\n    'label',\n    'text',\n    'value',\n    'description',\n    'linkedinText',\n  ];\n\n  for (const key of commonKeys) {\n    const text = scalarText(value[key], preferredKeys);\n    if (text) return text;\n  }\n\n  return '';\n};\n\nconst skillsText = (value) => {\n  const values = Array.isArray(value) ? value : [value];\n  return [...new Set(\n    values\n      .map((item) => scalarText(item, ['skillName', 'skill', 'name', 'title']))\n      .filter(Boolean),\n  )].join(', ');\n};\n\nconst records = $input.all().map((item) => item.json);\nconst candidates = records\n  .filter((record) => record && !record.error && (record.profileUrl || record.linkedinUrl || record.url))\n  .map((record) => ({\n    name: [record.firstName, record.lastName].filter(Boolean).join(' ').trim() ||\n      record.fullName || 'LinkedIn profile',\n    title: scalarText(record.headline || record.jobTitle, ['headline', 'jobTitle']),\n    location: scalarText(\n      record.location || record.geoLocation,\n      ['linkedinText', 'full', 'city', 'region', 'country'],\n    ),\n    skills: skillsText(record.skills),\n    profile_url: record.profileUrl || record.linkedinUrl || record.url,\n    source: 'linkedin',\n    email: scalarText(record.email),\n    bio_snippet: scalarText(record.about || record.summary),\n    source_metrics: {},\n  }));\n\nreturn candidates.length\n  ? candidates.map((candidate) => ({ json: candidate }))\n  : [{ json: { _source_empty: true, source: 'linkedin' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "b79d1168-654b-4acf-a070-8ecafb94770f",
      "name": "Search GitHub Profiles by Role and Location",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        16000,
        6416
      ],
      "parameters": {
        "url": "https://api.github.com/search/users",
        "options": {
          "timeout": 15000
        },
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $json.github_user_query }}"
            },
            {
              "name": "per_page",
              "value": "={{ Math.min(10, Math.max(5, $json.max_candidates_per_source * 2)) }}"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            },
            {
              "name": "X-GitHub-Api-Version",
              "value": "2026-03-10"
            },
            {
              "name": "User-Agent",
              "value": "talent-atlas-n8n-workflow"
            }
          ]
        }
      },
      "executeOnce": true,
      "retryOnFail": true,
      "typeVersion": 4.4,
      "alwaysOutputData": true,
      "waitBetweenTries": 1000
    },
    {
      "id": "a44310a5-ce23-463c-9089-f99fbb2304dc",
      "name": "Split GitHub Results",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        16224,
        6416
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "items"
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "b4ec643a-23ca-4ee1-8f55-08e3bc402081",
      "name": "Search Each GitHub User Repositories",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        16448,
        6416
      ],
      "parameters": {
        "url": "=https://api.github.com/users/{{ $json.login }}/repos",
        "options": {
          "timeout": 15000
        },
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "type",
              "value": "owner"
            },
            {
              "name": "sort",
              "value": "pushed"
            },
            {
              "name": "direction",
              "value": "desc"
            },
            {
              "name": "per_page",
              "value": "30"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            },
            {
              "name": "X-GitHub-Api-Version",
              "value": "2026-03-10"
            },
            {
              "name": "User-Agent",
              "value": "talent-atlas-n8n-workflow"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.4,
      "alwaysOutputData": true,
      "waitBetweenTries": 750
    },
    {
      "id": "ed593b5d-f4e2-475d-a811-a0011d33461b",
      "name": "Group GitHub Repositories by Owner",
      "type": "n8n-nodes-base.code",
      "position": [
        16672,
        6416
      ],
      "parameters": {
        "jsCode": "const brief = $('Build Search Brief').first().json;\nconst owners = new Map();\n\nfor (const item of $input.all()) {\n  const repo = item.json || {};\n  const owner = repo.owner || {};\n  if (owner.type !== 'User' || !owner.login || repo.fork || repo.archived) continue;\n\n  const login = owner.login;\n  if (!owners.has(login)) {\n    owners.set(login, {\n      github_login: login,\n      profile_api_url: owner.url || 'https://api.github.com/users/' + encodeURIComponent(login),\n      repositories: [],\n    });\n  }\n\n  const record = owners.get(login);\n  record.repositories.push(repo);\n}\n\nconst candidates = [...owners.values()]\n  .map((owner) => {\n    const terms = [\n      ...(brief.github_repository_terms || []),\n      String(brief.github_language || '').toLowerCase(),\n    ];\n    const rankedRepositories = owner.repositories\n      .map((repo) => {\n        const evidence = [\n          repo.name,\n          repo.description,\n          repo.language,\n          ...(Array.isArray(repo.topics) ? repo.topics : []),\n        ].filter(Boolean).join(' ').toLowerCase();\n        const termMatches = terms.filter((term) =>\n          evidence.includes(String(term).toLowerCase())\n        ).length;\n        const pushedAt = Date.parse(repo.pushed_at || '');\n        const recent = Number.isFinite(pushedAt) &&\n          Date.now() - pushedAt <= 1000 * 60 * 60 * 24 * 365 * 2;\n        return {\n          repo,\n          relevance: termMatches * 10 +\n            (recent ? 3 : 0) +\n            Math.min(5, Math.log10(Number(repo.stargazers_count || 0) + 1) * 2),\n          termMatches,\n        };\n      })\n      .sort((a, b) => b.relevance - a.relevance)\n      .slice(0, 5);\n\n    const selected = rankedRepositories.map((entry) => entry.repo);\n    const repositoryNames = selected.map((repo) => repo.full_name || repo.name || '');\n    const repositoryDescriptions = selected.map((repo) => repo.description).filter(Boolean);\n    const repositoryLanguages = [...new Set(selected.map((repo) => repo.language).filter(Boolean))];\n    const repositoryTopics = [...new Set(selected.flatMap((repo) => Array.isArray(repo.topics) ? repo.topics : []))];\n    const relevantRepositoryCount = rankedRepositories.filter((entry) => entry.termMatches > 0).length;\n\n    return {\n      github_login: owner.github_login,\n      profile_api_url: owner.profile_api_url,\n      repository_names: repositoryNames,\n      repository_urls: selected.map((repo) => repo.html_url || ''),\n      repository_descriptions: repositoryDescriptions,\n      repository_languages: repositoryLanguages,\n      repository_topics: repositoryTopics,\n      repository_stars: selected.reduce((sum, repo) => sum + Number(repo.stargazers_count || 0), 0),\n      repository_forks: selected.reduce((sum, repo) => sum + Number(repo.forks_count || 0), 0),\n      latest_repo_push: selected.map((repo) => repo.pushed_at || '').sort().at(-1) || '',\n      relevant_repository_count: relevantRepositoryCount,\n      repository_evidence: [\n        repositoryNames.length && 'Repositories: ' + repositoryNames.join(', '),\n        repositoryDescriptions.length && 'Descriptions: ' + repositoryDescriptions.join(' | '),\n        repositoryLanguages.length && 'Languages: ' + repositoryLanguages.join(', '),\n        repositoryTopics.length && 'Topics: ' + repositoryTopics.join(', '),\n      ].filter(Boolean).join('. '),\n    };\n  })\n  .sort((a, b) =>\n    b.relevant_repository_count - a.relevant_repository_count ||\n    b.repository_stars - a.repository_stars\n  )\n  .slice(0, Math.min(10, Math.max(5, brief.max_candidates_per_source * 2)));\n\nreturn candidates.length\n  ? candidates.map((candidate) => ({ json: candidate }))\n  : [{ json: { _source_empty: true, source: 'github' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "bf97a24a-d6a9-4e4d-a986-3b1a35067d25",
      "name": "Fetch GitHub Profiles",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        16896,
        6352
      ],
      "parameters": {
        "url": "={{ $json.profile_api_url }}",
        "options": {
          "timeout": 10000
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "application/vnd.github+json"
            },
            {
              "name": "X-GitHub-Api-Version",
              "value": "2026-03-10"
            },
            {
              "name": "User-Agent",
              "value": "talent-atlas-n8n-workflow"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.4,
      "alwaysOutputData": true,
      "waitBetweenTries": 500
    },
    {
      "id": "cde40777-e0b5-43da-b2b4-ffd560d70678",
      "name": "Merge GitHub Profile and Repository Evidence",
      "type": "n8n-nodes-base.merge",
      "position": [
        17120,
        6416
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2
    },
    {
      "id": "74790fb9-b2c9-4741-93a5-ec8f4bfda4d9",
      "name": "Normalize GitHub",
      "type": "n8n-nodes-base.code",
      "position": [
        17344,
        6416
      ],
      "parameters": {
        "jsCode": "const records = $input.all().map((item) => item.json);\nconst candidates = records\n  .filter((record) =>\n    record &&\n    !record.error &&\n    record.html_url &&\n    record.github_login &&\n    !record._source_empty\n  )\n  .map((record) => ({\n    name: record.name || record.login || 'GitHub profile',\n    title: record.bio || 'Active open-source developer',\n    location: record.location || '',\n    skills: [\n      ...(record.repository_languages || []),\n      ...(record.repository_topics || []),\n    ].filter(Boolean).join(', '),\n    profile_url: record.html_url,\n    source: 'github',\n    email: record.email || '',\n    bio_snippet: [\n      record.bio,\n      record.repository_evidence,\n    ].filter(Boolean).join('. ').slice(0, 1400),\n    source_metrics: {\n      followers: Number(record.followers || 0),\n      public_repositories: Number(record.public_repos || 0),\n      relevant_repositories: Number(record.relevant_repository_count || 0),\n      repository_stars: Number(record.repository_stars || 0),\n      repository_forks: Number(record.repository_forks || 0),\n      latest_repo_push: record.latest_repo_push || '',\n    },\n  }));\n\nreturn candidates.length\n  ? candidates.map((candidate) => ({ json: candidate }))\n  : [{ json: { _source_empty: true, source: 'github' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "5c32138b-7ded-4b2e-bda7-bfc29e50bb01",
      "name": "Search Stack Overflow Experts",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        16448,
        6736
      ],
      "parameters": {
        "url": "={{ 'https://api.stackexchange.com/2.3/tags/' + encodeURIComponent($json.stackoverflow_tag) + '/top-answerers/all_time' }}",
        "options": {
          "timeout": 15000
        },
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "site",
              "value": "stackoverflow"
            },
            {
              "name": "pagesize",
              "value": "={{ $json.max_candidates_per_source }}"
            },
            {
              "name": "order",
              "value": "desc"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "talent-atlas-n8n-workflow"
            }
          ]
        }
      },
      "executeOnce": true,
      "retryOnFail": true,
      "typeVersion": 4.4,
      "alwaysOutputData": true,
      "waitBetweenTries": 1000
    },
    {
      "id": "0628c77a-df1c-4bfe-a90e-8be2f2837a4d",
      "name": "Split Stack Overflow Results",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        16672,
        6736
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "items"
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "7d863d77-4212-431a-b7de-261adcd12245",
      "name": "Normalize Stack Overflow",
      "type": "n8n-nodes-base.code",
      "position": [
        16896,
        6736
      ],
      "parameters": {
        "jsCode": "const brief = $('Build Search Brief').first().json;\nconst records = $input.all().map((item) => item.json);\nconst candidates = records\n  .filter((record) => record?.user?.link)\n  .map((record) => ({\n    name: record.user.display_name || 'Stack Overflow profile',\n    title: 'Top ' + brief.stackoverflow_tag + ' answerer',\n    location: brief.location,\n    skills: brief.stackoverflow_tag,\n    profile_url: record.user.link,\n    source: 'stackoverflow',\n    email: '',\n    bio_snippet:\n      'Top-answerer evidence for ' + brief.stackoverflow_tag +\n      '; answer score ' + Number(record.score || 0) +\n      '; post count ' + Number(record.post_count || 0) + '.',\n    source_metrics: {\n      answer_score: Number(record.score || 0),\n      post_count: Number(record.post_count || 0),\n      reputation: Number(record.user.reputation || 0),\n    },\n  }));\n\nreturn candidates.length\n  ? candidates.map((candidate) => ({ json: candidate }))\n  : [{ json: { _source_empty: true, source: 'stackoverflow' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "6ff587e5-175f-42d7-96ae-10a74dd3aeeb",
      "name": "Find Latest HN Hiring Thread",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        16000,
        7072
      ],
      "parameters": {
        "url": "https://hn.algolia.com/api/v1/search_by_date",
        "options": {
          "timeout": 10000
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "query",
              "value": "Who wants to be hired"
            },
            {
              "name": "tags",
              "value": "story"
            },
            {
              "name": "hitsPerPage",
              "value": "50"
            }
          ]
        }
      },
      "executeOnce": true,
      "retryOnFail": true,
      "typeVersion": 4.4,
      "alwaysOutputData": true,
      "waitBetweenTries": 1000
    },
    {
      "id": "292f5c85-a19b-4512-acbb-76544f1e5760",
      "name": "Select Exact HN Hiring Thread",
      "type": "n8n-nodes-base.code",
      "position": [
        16224,
        7072
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst hits = Array.isArray(input.hits) ? input.hits : [];\nconst exactTitle = /^Ask HN:\\s*Who wants to be hired\\?\\s*(?:\\(|$)/i;\nconst selected = hits.find((hit) => exactTitle.test(String(hit.title || '')));\n\nreturn [{\n  json: selected\n    ? {\n        story_id: selected.objectID,\n        story_title: selected.title,\n        story_created_at: selected.created_at,\n      }\n    : {\n        story_id: 'missing',\n        story_title: '',\n        story_created_at: '',\n      },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "7392f29c-b503-48c6-8cc1-20d428e9cf5d",
      "name": "Search HN Candidates",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        16448,
        7072
      ],
      "parameters": {
        "url": "https://hn.algolia.com/api/v1/search",
        "options": {
          "timeout": 15000
        },
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "query",
              "value": "={{ $('Build Search Brief').first().json.primary_skill }}"
            },
            {
              "name": "tags",
              "value": "={{ 'comment,story_' + $json.story_id }}"
            },
            {
              "name": "hitsPerPage",
              "value": "={{ $('Build Search Brief').first().json.max_candidates_per_source }}"
            },
            {
              "name": "restrictSearchableAttributes",
              "value": "comment_text"
            }
          ]
        }
      },
      "executeOnce": true,
      "retryOnFail": true,
      "typeVersion": 4.4,
      "alwaysOutputData": true,
      "waitBetweenTries": 1000
    },
    {
      "id": "4af25e1b-5b56-4567-aec4-c2b59867c515",
      "name": "Split HN Results",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        16672,
        7072
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "hits"
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "3782dac7-70e3-47b8-9eaf-cecdb41ffe9c",
      "name": "Normalize Hacker News",
      "type": "n8n-nodes-base.code",
      "position": [
        16896,
        7072
      ],
      "parameters": {
        "jsCode": "const decodeText = (value) => String(value || '')\n  .replace(/<p>/gi, '\\n')\n  .replace(/<[^>]+>/g, ' ')\n  .replace(/&gt;/g, '>')\n  .replace(/&lt;/g, '<')\n  .replace(/&amp;/g, '&')\n  .replace(/&#x27;/g, \"'\")\n  .replace(/&quot;/g, '\"')\n  .replace(/\\s+/g, ' ')\n  .trim();\n\nconst records = $input.all().map((item) => item.json);\nconst candidates = records\n  .filter((record) => record?.author && record?.comment_text)\n  .map((record) => {\n    const text = decodeText(record.comment_text);\n    const location = text.match(/\\b(?:location|based in)\\s*[:\\-]\\s*([^|;]{2,80})/i)?.[1]?.trim() || '';\n    const email = text.match(/[a-zA-Z0-9._%+\\-]+@[a-zA-Z0-9.\\-]+\\.[a-zA-Z]{2,}/)?.[0] || '';\n    return {\n      name: record.author,\n      title: text.slice(0, 140),\n      location,\n      skills: text.slice(0, 500),\n      profile_url: 'https://news.ycombinator.com/user?id=' + encodeURIComponent(record.author),\n      source: 'hackernews',\n      email,\n      bio_snippet: text.slice(0, 700),\n      source_metrics: {},\n    };\n  });\n\nreturn candidates.length\n  ? candidates.map((candidate) => ({ json: candidate }))\n  : [{ json: { _source_empty: true, source: 'hackernews' } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "da2e27f5-65fc-4e1c-b930-9bb65a348107",
      "name": "Merge LinkedIn and GitHub",
      "type": "n8n-nodes-base.merge",
      "position": [
        17568,
        6224
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "45bd53f5-eb73-4a10-9120-1822c3583c1b",
      "name": "Merge Stack Overflow and HN",
      "type": "n8n-nodes-base.merge",
      "position": [
        17120,
        6816
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "1b8ca81f-eb62-49dc-a021-e210b18fefe5",
      "name": "Merge All Sources",
      "type": "n8n-nodes-base.merge",
      "position": [
        17840,
        6416
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "b038f93a-c82f-4877-862a-1fc72f89cbf4",
      "name": "Sanitize Candidate Pool",
      "type": "n8n-nodes-base.code",
      "position": [
        18112,
        6416
      ],
      "parameters": {
        "jsCode": "const brief = $('Build Search Brief').first().json;\nconst seen = new Set();\nconst candidates = [];\n\nconst skillAliases = {\n  '.net core': [\n    '.net core',\n    'asp.net core',\n    'aspnet core',\n    'aspnetcore',\n    'aspnet-core',\n    'dotnet core',\n    'dotnet-core',\n    'net core',\n  ],\n  '.net': ['.net', 'dotnet', 'asp.net'],\n  'c#': ['c#', 'c sharp', 'csharp'],\n  'react.js': ['react.js', 'reactjs', 'react ', 'react-'],\n  'node.js': ['node.js', 'nodejs'],\n  'rest apis': ['rest api', 'rest-api', 'restful api', 'web api', 'webapi'],\n  'sql server': ['sql server', 'sql-server', 'mssql'],\n  'microservices': ['microservice'],\n  'ci/cd': ['ci/cd', 'ci cd', 'continuous integration', 'continuous delivery'],\n  'kubernetes': ['kubernetes', 'k8s'],\n};\n\nconst containsSkill = (evidence, skill) => {\n  const key = String(skill || '').trim().toLowerCase();\n  const aliases = skillAliases[key] || [key];\n  return aliases.some((alias) =>\n    (alias === 'c#' || alias.length >= 3) && evidence.includes(alias)\n  );\n};\n\nconst titleTerms = String(brief.job_title || '')\n  .toLowerCase()\n  .split(/[^a-z0-9+#.]+/)\n  .filter((term) => term.length >= 4 && !['senior', 'developer', 'engineer'].includes(term));\n\nfor (const item of $input.all()) {\n  const candidate = item.json || {};\n  if (candidate._source_empty || candidate.error || !candidate.profile_url) continue;\n\n  const key = String(candidate.profile_url).trim().toLowerCase() ||\n    [candidate.source, candidate.name].join(':').toLowerCase();\n  if (seen.has(key)) continue;\n  seen.add(key);\n\n  const normalized = {\n    name: String(candidate.name || 'Unknown candidate').trim(),\n    title: String(candidate.title || '').trim(),\n    location: String(candidate.location || '').trim(),\n    skills: String(candidate.skills || '').trim(),\n    profile_url: String(candidate.profile_url || '').trim(),\n    source: String(candidate.source || '').trim(),\n    email: String(candidate.email || '').trim(),\n    bio_snippet: String(candidate.bio_snippet || '').trim().slice(0, 1200),\n    source_metrics: candidate.source_metrics || {},\n  };\n\n  const evidence = [\n    normalized.title,\n    normalized.skills,\n    normalized.bio_snippet,\n  ].join(' ').toLowerCase();\n  const prefilterMatchedSkills = (brief.must_have_skills || [])\n    .filter((skill) => containsSkill(evidence, skill));\n  const coverage = brief.must_have_skills?.length\n    ? prefilterMatchedSkills.length / brief.must_have_skills.length\n    : 0;\n  const titleMatches = titleTerms.filter((term) =>\n    normalized.title.toLowerCase().includes(term)\n  ).length;\n  const requestedLocation = String(brief.location || '').toLowerCase();\n  const locationMatch = !requestedLocation ||\n    requestedLocation === 'remote' ||\n    normalized.location.toLowerCase().includes(requestedLocation) ||\n    normalized.location.toLowerCase().includes('remote');\n\n  candidates.push({\n    ...normalized,\n    prefilter_matched_skills: prefilterMatchedSkills,\n    prefilter_score: Math.round(\n      coverage * 80 +\n      Math.min(15, titleMatches * 5) +\n      (locationMatch ? 5 : 0),\n    ),\n  });\n}\n\n// Keep the shortlist compact for fast deterministic evaluation and summary generation.\nconst cap = Math.min(\n  10,\n  Math.max(1, brief.max_results, brief.max_candidates_per_source * 2),\n);\nconst limited = candidates\n  .sort((a, b) => b.prefilter_score - a.prefilter_score)\n  .slice(0, cap);\n\nreturn limited.length\n  ? limited.map((candidate) => ({ json: { ...candidate, _empty: false } }))\n  : [{ json: { _empty: true } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "e4c60b21-f758-42ac-8614-26ef6545456b",
      "name": "Candidates Found?",
      "type": "n8n-nodes-base.if",
      "position": [
        18384,
        6416
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "27f171a1-2a16-4441-b244-dbee93f19bf5",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json._empty }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "932a21d9-2e2d-47b1-a4c9-fabbfebdc0de",
      "name": "Calculate Evidence Scores",
      "type": "n8n-nodes-base.code",
      "position": [
        18464,
        5984
      ],
      "parameters": {
        "jsCode": "const brief = $('Build Search Brief').first().json;\nconst mustHave = Array.isArray(brief.must_have_skills) ? brief.must_have_skills : [];\nconst niceToHave = Array.isArray(brief.nice_to_have_skills) ? brief.nice_to_have_skills : [];\n\nconst skillAliases = {\n  '.net core': ['.net core', 'asp.net core', 'aspnet core', 'aspnetcore', 'aspnet-core', 'dotnet core', 'dotnet-core'],\n  '.net': ['.net', 'dotnet', 'asp.net'],\n  'c#': ['c#', 'c sharp', 'csharp'],\n  'react.js': ['react.js', 'reactjs', 'react ', 'react-'],\n  'node.js': ['node.js', 'nodejs'],\n  'rest apis': ['rest api', 'rest-api', 'restful api', 'web api', 'webapi'],\n  'sql server': ['sql server', 'sql-server', 'mssql'],\n  'microservices': ['microservice'],\n  'ci/cd': ['ci/cd', 'ci cd', 'continuous integration', 'continuous delivery'],\n  'kubernetes': ['kubernetes', 'k8s'],\n};\n\nconst containsSkill = (evidence, skill) => {\n  const key = String(skill || '').trim().toLowerCase();\n  const aliases = skillAliases[key] || [key];\n  return aliases.some((alias) =>\n    (alias === 'c#' || alias.length >= 3) && evidence.includes(alias)\n  );\n};\n\nconst jobTitleTerms = String(brief.job_title || '')\n  .toLowerCase()\n  .split(/[^a-z0-9+#.]+/)\n  .filter((term) =>\n    term.length >= 4 &&\n    !['senior', 'junior', 'lead', 'developer', 'engineer'].includes(term)\n  );\nconst requestedLocation = String(brief.location || '').trim().toLowerCase();\nconst requiredYears = Number(String(brief.experience || '').match(/\\d+/)?.[0] || 0);\n\nconst evidenceQualityScore = (candidate) => {\n  const metrics = candidate.source_metrics || {};\n  if (candidate.source === 'linkedin') {\n    return Math.min(\n      10,\n      (String(candidate.skills || '').length >= 80 ? 5 : 2) +\n      (String(candidate.bio_snippet || '').length >= 300 ? 5 : 2),\n    );\n  }\n  if (candidate.source === 'github') {\n    const latestPush = Date.parse(metrics.latest_repo_push || '');\n    const recent = Number.isFinite(latestPush) &&\n      Date.now() - latestPush <= 1000 * 60 * 60 * 24 * 365 * 2;\n    return Math.min(\n      10,\n      Math.min(6, Number(metrics.relevant_repositories || 0) * 3) +\n      (recent ? 2 : 0) +\n      (Number(metrics.repository_stars || 0) >= 5 ? 2 : 0),\n    );\n  }\n  if (candidate.source === 'stackoverflow') {\n    return Math.min(\n      8,\n      (Number(metrics.answer_score || 0) >= 1000 ? 5 : 2) +\n      (Number(metrics.post_count || 0) >= 100 ? 3 : 1),\n    );\n  }\n  if (candidate.source === 'hackernews') {\n    return Math.min(\n      8,\n      (String(candidate.bio_snippet || '').length >= 500 ? 6 : 3) +\n      (candidate.email ? 2 : 0),\n    );\n  }\n  return 4;\n};\n\nreturn $input.all().map((item) => {\n  const candidate = item.json;\n  const evidence = [\n    candidate.title,\n    candidate.skills,\n    candidate.bio_snippet,\n  ].join(' ').toLowerCase();\n\n  const matchedSkills = mustHave.filter((skill) => containsSkill(evidence, skill));\n  const missingSkills = mustHave.filter((skill) => !containsSkill(evidence, skill));\n  const niceMatches = niceToHave.filter((skill) => containsSkill(evidence, skill));\n  const coverage = mustHave.length ? matchedSkills.length / mustHave.length : 0;\n  const mustHaveScore = Math.round(coverage * 65);\n\n  const title = String(candidate.title || '').toLowerCase();\n  const titleMatches = jobTitleTerms.filter((term) => title.includes(term)).length;\n  const titleScore = jobTitleTerms.length\n    ? Math.round((titleMatches / jobTitleTerms.length) * 10)\n    : 5;\n\n  const location = String(candidate.location || '').trim().toLowerCase();\n  let locationScore = 0;\n  let locationStatus = 'unknown';\n  if (!requestedLocation || requestedLocation === 'remote') {\n    locationScore = 5;\n    locationStatus = 'not constrained';\n  } else if (location.includes(requestedLocation)) {\n    locationScore = 5;\n    locationStatus = 'matched';\n  } else if (location.includes('remote')) {\n    locationScore = 3;\n    locationStatus = 'remote';\n  } else if (location) {\n    locationStatus = 'different';\n  }\n\n  const years = [...evidence.matchAll(/(\\d{1,2})\\+?\\s*(?:years|yrs)/g)]\n    .map((match) => Number(match[1]))\n    .filter(Number.isFinite);\n  const maxYears = years.length ? Math.max(...years) : 0;\n  const seniorityEvidence = /\\b(senior|lead|principal|architect|staff)\\b/i.test(evidence);\n  const experienceScore = !requiredYears\n    ? 3\n    : maxYears >= requiredYears\n      ? 5\n      : seniorityEvidence\n        ? 3\n        : 0;\n\n  const sourceEvidenceScore = evidenceQualityScore(candidate);\n  const niceToHaveScore = niceToHave.length\n    ? Math.round((niceMatches.length / niceToHave.length) * 5)\n    : 0;\n  const score = Math.min(\n    100,\n    mustHaveScore +\n      titleScore +\n      locationScore +\n      experienceScore +\n      sourceEvidenceScore +\n      niceToHaveScore,\n  );\n\n  const concerns = [];\n  if (coverage < 0.5) concerns.push('Evidence covers fewer than half of the must-have skills');\n  if (locationStatus === 'unknown') concerns.push('Location is not available from this source');\n  if (locationStatus === 'different') concerns.push('Candidate location does not match the requested location');\n  if (requiredYears && !maxYears && !seniorityEvidence) concerns.push('Required experience duration is not verified');\n  if (candidate.source === 'github') concerns.push('Repository ownership is evidence of work, but contribution depth should be reviewed');\n  if (candidate.source === 'stackoverflow') concerns.push('Strong specialist evidence does not verify full employment history');\n  if (candidate.source === 'hackernews') concerns.push('Profile details are self-reported');\n\n  const recommendation = score >= 85\n    ? 'Strong evidence match'\n    : score >= 70\n      ? 'Good evidence match'\n      : score >= 55\n        ? 'Potential match - review'\n        : 'Low evidence match';\n  const summary = [\n    matchedSkills.length + '/' + mustHave.length + ' must-have skills evidenced',\n    titleScore >= 7 ? 'title aligned' : 'title only partially aligned',\n    locationStatus === 'matched' ? 'location matched' : 'location ' + locationStatus,\n    candidate.source + ' evidence quality ' + sourceEvidenceScore + '/10',\n  ].join('; ');\n\n  return {\n    json: {\n      ...candidate,\n      score,\n      score_method: 'deterministic-evidence-v1',\n      score_breakdown: {\n        must_have_skills: mustHaveScore,\n        title_alignment: titleScore,\n        location: locationScore,\n        experience: experienceScore,\n        source_evidence: sourceEvidenceScore,\n        nice_to_have: niceToHaveScore,\n      },\n      recommendation,\n      summary,\n      matched_skills: matchedSkills,\n      missing_skills: missingSkills,\n      nice_to_have_matches: niceMatches,\n      concerns,\n      request_id: brief.request_id,\n      job_title: brief.job_title,\n      jd_keywords: brief.skills_str,\n      created_at: new Date().toISOString(),\n    },\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "e75b5e6c-6119-4e4c-9b01-4b100cf19dcc",
      "name": "Build Recruiter Response",
      "type": "n8n-nodes-base.code",
      "position": [
        18688,
        5888
      ],
      "parameters": {
        "jsCode": "const brief = $('Build Search Brief').first().json;\nconst scored = $input.all()\n  .map((item) => item.json)\n  .sort((a, b) => Number(b.score || 0) - Number(a.score || 0));\n\nconst qualified = scored\n  .filter((candidate) => Number(candidate.score || 0) >= brief.score_threshold)\n  .slice(0, brief.max_results);\n\nconst bySource = {};\nfor (const candidate of scored) {\n  bySource[candidate.source] = (bySource[candidate.source] || 0) + 1;\n}\n\nreturn [{\n  json: {\n    ok: true,\n    workflow: 'Talent Atlas - Multi-Source Candidate Discovery and Evidence Shortlisting',\n    version: '1.1.0',\n    request_id: brief.request_id,\n    generated_at: new Date().toISOString(),\n    search_criteria: {\n      job_title: brief.job_title,\n      location: brief.location,\n      experience: brief.experience,\n      seniority: brief.seniority,\n      employment_type: brief.employment_type,\n      skills: brief.skills,\n      must_have_skills: brief.must_have_skills,\n      nice_to_have_skills: brief.nice_to_have_skills,\n      score_threshold: brief.score_threshold,\n    },\n    stats: {\n      candidates_evaluated: scored.length,\n      candidates_qualified: qualified.length,\n      candidates_returned: qualified.length,\n      by_source: bySource,\n    },\n    candidates: qualified,\n    human_review_required: true,\n    notice:\n      'Candidate scores use a transparent evidence formula, not AI judgment. Verify candidate data and use a qualified human reviewer for all hiring decisions.',\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f04d3bdc-8cb5-4017-8c50-05ac628929b9",
      "name": "Build Shortlist Summary Input",
      "type": "n8n-nodes-base.code",
      "position": [
        18912,
        5888
      ],
      "parameters": {
        "jsCode": "const report = $input.first().json;\nreturn [{\n  json: {\n    request_id: report.request_id,\n    search_criteria: report.search_criteria,\n    stats: report.stats,\n    candidates: (report.candidates || []).slice(0, 10).map((candidate) => ({\n      name: candidate.name,\n      source: candidate.source,\n      score: candidate.score,\n      recommendation: candidate.recommendation,\n      summary: candidate.summary,\n      matched_skills: candidate.matched_skills,\n      missing_skills: candidate.missing_skills,\n      concerns: candidate.concerns,\n    })),\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "e9bfa5bd-931d-4cc8-9fb4-9b3acb6111f2",
      "name": "Summarize Shortlist",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        19136,
        5888
      ],
      "parameters": {
        "text": "={{ 'Summarize this evidence-ranked shortlist for a recruiter. Do not change scores, invent facts, or make hiring decisions. Identify the strongest evidence, common gaps, source limitations, and useful verification steps.\\n\\nSHORTLIST:\\n' + JSON.stringify($json) }}",
        "options": {
          "systemMessage": "You are a recruiting research assistant. The ranking and scores are already calculated by deterministic rules. Explain the shortlist concisely without rescoring candidates. Do not infer protected characteristics. Return only the structured object required by the output parser."
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "retryOnFail": true,
      "typeVersion": 3.1,
      "waitBetweenTries": 3000
    },
    {
      "id": "e4698e3b-74a3-4686-b805-5e364312ea85",
      "name": "Model - Groq Shortlist Summary",
      "type": "@n8n/n8n-nodes-langchain.lmChatGroq",
      "position": [
        19152,
        6112
      ],
      "parameters": {
        "model": "llama-3.1-8b-instant",
        "options": {
          "temperature": 0.2,
          "maxTokensToSample": 700
        }
      },
      "typeVersion": 1
    },
    {
      "id": "3a695f68-934c-43a5-a9d2-06065ff7dcdb",
      "name": "Shortlist Summary Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        19280,
        6112
      ],
      "parameters": {
        "jsonSchemaExample": "{\"executive_summary\":\"Three candidates show strong evidence for the role.\",\"strongest_candidate_names\":[\"Candidate A\",\"Candidate B\"],\"common_strengths\":[\"Backend and cloud experience\"],\"common_gaps\":[\"Some profiles lack verified location\"],\"recommended_verification_steps\":[\"Confirm recent hands-on project ownership\",\"Validate location and availability\"]}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "5faefeaa-d71f-4736-ac7c-ff565d784fdc",
      "name": "Attach Shortlist Summary",
      "type": "n8n-nodes-base.code",
      "position": [
        19488,
        5888
      ],
      "parameters": {
        "jsCode": "const report = $('Build Recruiter Response').first().json;\nconst input = $input.first().json;\nconst summary = input.output && typeof input.output === 'object'\n  ? input.output\n  : {\n      executive_summary: 'AI summary was unavailable; deterministic scores and candidate evidence are still complete.',\n      strongest_candidate_names: [],\n      common_strengths: [],\n      common_gaps: [],\n      recommended_verification_steps: [],\n    };\n\nreturn [{\n  json: {\n    ...report,\n    ai_shortlist_summary: summary,\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "a197f1b9-d68f-4666-94d3-78f560ffce0f",
      "name": "Respond 200 - Results",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        19712,
        5888
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "json",
        "responseBody": "={{ $json }}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "94055937-97ce-426a-8281-834e3a112b17",
      "name": "Build No Results Response",
      "type": "n8n-nodes-base.code",
      "position": [
        18608,
        6608
      ],
      "parameters": {
        "jsCode": "const brief = $('Build Search Brief').first().json;\nreturn [{\n  json: {\n    ok: true,\n    workflow: 'Talent Atlas - Multi-Source Candidate Discovery and Evidence Shortlisting',\n    version: '1.1.0',\n    request_id: brief.request_id,\n    generated_at: new Date().toISOString(),\n    search_criteria: {\n      job_title: brief.job_title,\n      location: brief.location,\n      skills: brief.skills,\n    },\n    stats: {\n      candidates_evaluated: 0,\n      candidates_qualified: 0,\n      candidates_returned: 0,\n      by_source: {},\n    },\n    candidates: [],\n    human_review_required: true,\n    notice:\n      'No usable candidates were returned. Check source credentials, API limits, location breadth, and search terms.',\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "594b14b9-4008-4242-8b94-1ae0aa2797c4",
      "name": "Respond 200 - No Results",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        18832,
        6608
      ],
      "parameters": {
        "options": {
          "responseCode": 200
        },
        "respondWith": "json",
        "responseBody": "={{ $json }}"
      },
      "typeVersion": 1.5
    },
    {
      "id": "edf5b0a7-38d8-46e1-9b15-4de6cf211331",
      "name": "Save Results Enabled?",
      "type": "n8n-nodes-base.if",
      "position": [
        18688,
        6448
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "8ec086cc-9fa2-4a63-9988-b837fa7e21b4",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $('Build Search Brief').first().json.save_results }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "140d5242-ab17-4c14-a3fb-3d437baa7079",
      "name": "Save Candidate to Supabase",
      "type": "n8n-nodes-base.supabase",
      "onError": "continueRegularOutput",
      "maxTries": 2,
      "position": [
        18912,
        6448
      ],
      "parameters": {
        "tableId": "candidates",
        "fieldsUi": {
          "fieldValues": [
            {
              "fieldId": "request_id",
              "fieldValue": "={{ $json.request_id }}"
            },
            {
              "fieldId": "name",
              "fieldValue": "={{ $json.name }}"
            },
            {
              "fieldId": "title",
              "fieldValue": "={{ $json.title }}"
            },
            {
              "fieldId": "location",
              "fieldValue": "={{ $json.location }}"
            },
            {
              "fieldId": "skills",
              "fieldValue": "={{ $json.skills }}"
            },
            {
              "fieldId": "profile_url",
              "fieldValue": "={{ $json.profile_url }}"
            },
            {
              "fieldId": "source",
              "fieldValue": "={{ $json.source }}"
            },
            {
              "fieldId": "email",
              "fieldValue": "={{ $json.email }}"
            },
            {
              "fieldId": "score",
              "fieldValue": "={{ $json.score }}"
            },
            {
              "fieldId": "summary",
              "fieldValue": "={{ $json.summary }}"
            },
            {
              "fieldId": "recommendation",
              "fieldValue": "={{ $json.recommendation }}"
            },
            {
              "fieldId": "matched_skills",
              "fieldValue": "={{ $json.matched_skills }}"
            },
            {
              "fieldId": "missing_skills",
              "fieldValue": "={{ $json.missing_skills }}"
            },
            {
              "fieldId": "concerns",
              "fieldValue": "={{ $json.concerns }}"
            },
            {
              "fieldId": "jd_keywords",
              "fieldValue": "={{ $json.jd_keywords }}"
            },
            {
              "fieldId": "job_title",
              "fieldValue": "={{ $json.job_title }}"
            },
            {
              "fieldId": "bio_snippet",
              "fieldValue": "={{ $json.bio_snippet }}"
            },
            {
              "fieldId": "created_at",
              "fieldValue": "={{ $json.created_at }}"
            }
          ]
        }
      },
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 1000
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "2ea19562-f9ab-44f1-8a0c-d9cc4592d127",
  "nodeGroups": [],
  "connections": {
    "Normalize GitHub": {
      "main": [
        [
          {
            "node": "Merge LinkedIn and GitHub",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Split HN Results": {
      "main": [
        [
          {
            "node": "Normalize Hacker News",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Candidates Found?": {
      "main": [
        [
          {
            "node": "Calculate Evidence Scores",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build No Results Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge All Sources": {
      "main": [
        [
          {
            "node": "Sanitize Candidate Pool",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request Is Valid?": {
      "main": [
        [
          {
            "node": "Extract JD Requirements",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Invalid Request Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Search Brief": {
      "main": [
        [
          {
            "node": "Search LinkedIn via Apify",
            "type": "main",
            "index": 0
          },
          {
            "node": "Search GitHub Profiles by Role and Location",
            "type": "main",
            "index": 0
          },
          {
            "node": "Search Stack Overflow Experts",
            "type": "main",
            "index": 0
          },
          {
            "node": "Find Latest HN Hiring Thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize LinkedIn": {
      "main": [
        [
          {
            "node": "Merge LinkedIn and GitHub",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Summarize Shortlist": {
      "main": [
        [
          {
            "node": "Attach Shortlist Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search HN Candidates": {
      "main": [
        [
          {
            "node": "Split HN Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split GitHub Results": {
      "main": [
        [
          {
            "node": "Search Each GitHub User Repositories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch GitHub Profiles": {
      "main": [
        [
          {
            "node": "Merge GitHub Profile and Repository Evidence",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Hacker News": {
      "main": [
        [
          {
            "node": "Merge Stack Overflow and HN",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Save Results Enabled?": {
      "main": [
        [
          {
            "node": "Save Candidate to Supabase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JD Requirements Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Extract JD Requirements",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Extract JD Requirements": {
      "main": [
        [
          {
            "node": "Build Search Brief",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Webhook Input": {
      "main": [
        [
          {
            "node": "Request Is Valid?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sanitize Candidate Pool": {
      "main": [
        [
          {
            "node": "Candidates Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attach Shortlist Summary": {
      "main": [
        [
          {
            "node": "Respond 200 - Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Recruiter Response": {
      "main": [
        [
          {
            "node": "Build Shortlist Summary Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Candidate Search Webhook": {
      "main": [
        [
          {
            "node": "Normalize Webhook Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Stack Overflow": {
      "main": [
        [
          {
            "node": "Merge Stack Overflow and HN",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Shortlist Summary Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Summarize Shortlist",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Build No Results Response": {
      "main": [
        [
          {
            "node": "Respond 200 - No Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Evidence Scores": {
      "main": [
        [
          {
            "node": "Build Recruiter Response",
            "type": "main",
            "index": 0
          },
          {
            "node": "Save Results Enabled?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge LinkedIn and GitHub": {
      "main": [
        [
          {
            "node": "Merge All Sources",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search LinkedIn via Apify": {
      "main": [
        [
          {
            "node": "Normalize LinkedIn",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Model - Groq JD Extraction": {
      "ai_languageModel": [
        [
          {
            "node": "Extract JD Requirements",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Merge Stack Overflow and HN": {
      "main": [
        [
          {
            "node": "Merge All Sources",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Find Latest HN Hiring Thread": {
      "main": [
        [
          {
            "node": "Select Exact HN Hiring Thread",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Stack Overflow Results": {
      "main": [
        [
          {
            "node": "Normalize Stack Overflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Shortlist Summary Input": {
      "main": [
        [
          {
            "node": "Summarize Shortlist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Stack Overflow Experts": {
      "main": [
        [
          {
            "node": "Split Stack Overflow Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Select Exact HN Hiring Thread": {
      "main": [
        [
          {
            "node": "Search HN Candidates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Invalid Request Response": {
      "main": [
        [
          {
            "node": "Respond 400 - Invalid Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Model - Groq Shortlist Summary": {
      "ai_languageModel": [
        [
          {
            "node": "Summarize Shortlist",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Group GitHub Repositories by Owner": {
      "main": [
        [
          {
            "node": "Fetch GitHub Profiles",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge GitHub Profile and Repository Evidence",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Search Each GitHub User Repositories": {
      "main": [
        [
          {
            "node": "Group GitHub Repositories by Owner",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search GitHub Profiles by Role and Location": {
      "main": [
        [
          {
            "node": "Split GitHub Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge GitHub Profile and Repository Evidence": {
      "main": [
        [
          {
            "node": "Normalize GitHub",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}