{
  "name": "QuestGuard \u2014 BFSI Job Match Pipeline",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "analyze",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "93aab927-117c-4716-97b8-1b14d2287b57",
      "name": "1. Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        2520,
        640
      ]
    },
    {
      "parameters": {
        "jsCode": "const body = $json.body || $json;\n\nconst toArray = (value) => {\n  if (Array.isArray(value)) return value;\n  if (typeof value === 'string' && value.trim()) {\n    try {\n      const parsed = JSON.parse(value);\n      return Array.isArray(parsed) ? parsed : [];\n    } catch {\n      return [];\n    }\n  }\n  return [];\n};\n\nconst normalizeText = (value) => typeof value === 'string' ? value.trim() : '';\nconst normalizeList = (value) => Array.isArray(value)\n  ? value.map((item) => String(item).trim().toLowerCase()).filter(Boolean)\n  : [];\n\nconst jobsRaw = (() => {\n  const direct = toArray(body.jobs);\n  if (direct.length) return direct;\n  const feed = toArray(body.job_feed);\n  if (feed.length) return feed;\n  return [];\n})();\n\nconst structuredSkills = (() => {\n  if (Array.isArray(body.candidate_skills)) return body.candidate_skills;\n  if (Array.isArray(body.candidate_profile?.skills)) return body.candidate_profile.skills;\n  return [];\n})();\n\nconst targetSegments = normalizeList(body.target_segments);\nconst finalSegments = targetSegments.length ? targetSegments : ['banking', 'financial_services', 'insurance'];\nconst topK = Math.max(1, Math.min(Number(body.top_k || body.limit || 10) || 10, 20));\n\nconst candidateText = [\n  normalizeText(body.resume_text),\n  normalizeText(body.linkedin_text),\n  normalizeText(body.chat_summary),\n  normalizeText(body.candidate_summary),\n  normalizeText(body.candidate_profile?.summary),\n  normalizeText(body.candidate_profile?.headline),\n].filter(Boolean).join('\\n\\n');\n\nif (!jobsRaw.length) {\n  throw new Error('Provide a non-empty jobs array or job_feed array.');\n}\n\nif (!candidateText && structuredSkills.length === 0) {\n  throw new Error('Provide resume_text, linkedin_text, chat_summary, or candidate_profile.skills.');\n}\n\nreturn [{\n  json: {\n    payload_type: 'request',\n    candidate_name: normalizeText(body.candidate_name || body.name) || 'Candidate',\n    candidate_text: candidateText,\n    jobs_raw: jobsRaw,\n    candidate_preferences: body.candidate_preferences || {},\n    candidate_profile: body.candidate_profile || {},\n    chat_profile: body.chat_profile || {},\n    structured_skills: structuredSkills.map((item) => String(item).trim().toLowerCase()).filter(Boolean),\n    target_segments: finalSegments,\n    top_k: topK,\n    requested_at: new Date().toISOString(),\n    source_summary: {\n      jobs_received: jobsRaw.length,\n      candidate_text_present: Boolean(candidateText),\n      structured_skill_count: structuredSkills.length,\n      has_candidate_profile: Boolean(body.candidate_profile),\n      has_chat_profile: Boolean(body.chat_profile),\n    },\n  },\n}];"
      },
      "id": "9006c623-1939-48fd-b6f1-755d1048d86c",
      "name": "2. Candidate + Jobs Validator",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2780,
        640
      ]
    },
    {
      "parameters": {
        "jsCode": "const request = $('2. Candidate + Jobs Validator').item.json;\nconst clean = (request.candidate_text || '')\n  .replace(/\\r\\n/g, '\\n')\n  .replace(/\\t/g, ' ')\n  .replace(/ {2,}/g, ' ')\n  .trim();\nconst lines = clean.split('\\n').map((line) => line.trim()).filter(Boolean);\nconst lower = clean.toLowerCase();\nconst words = clean ? clean.split(/\\s+/).length : 0;\n\nconst cityPatterns = {\n  mumbai: /\\bmumbai\\b/,\n  bengaluru: /\\bbengaluru\\b|\\bbangalore\\b/,\n  pune: /\\bpune\\b/,\n  delhi_ncr: /\\bdelhi\\b|\\bgurugram\\b|\\bgurgaon\\b|\\bnoida\\b/,\n  hyderabad: /\\bhyderabad\\b/,\n  chennai: /\\bchennai\\b/,\n  kolkata: /\\bkolkata\\b/,\n  ahmedabad: /\\bahmedabad\\b/,\n  remote: /\\bremote\\b|\\bwork from home\\b/,\n};\n\nconst locationHints = Object.entries(cityPatterns)\n  .filter(([, pattern]) => pattern.test(lower))\n  .map(([city]) => city);\n\nreturn [{\n  json: {\n    payload_type: 'candidate_text',\n    candidate_name: request.candidate_name,\n    clean_text: clean,\n    lower,\n    lines,\n    words,\n    structured_skills: request.structured_skills || [],\n    location_hints: locationHints,\n    candidate_preferences: request.candidate_preferences,\n    chat_profile: request.chat_profile,\n    candidate_profile: request.candidate_profile,\n    target_segments: request.target_segments,\n    top_k: request.top_k,\n  },\n}];"
      },
      "id": "e0f5b3bf-f383-4cb7-80a2-8dfb467f4781",
      "name": "3a. Candidate Text Preprocessor",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3060,
        384
      ]
    },
    {
      "parameters": {
        "jsCode": "const data = $json;\nconst text = data.clean_text || '';\nconst lower = data.lower || '';\n\nconst extractMatches = (list) => list.filter((item) => lower.includes(item));\nconst unique = (items) => [...new Set(items.filter(Boolean))];\n\nconst SKILLS = [\n  'sql', 'python', 'excel', 'power bi', 'tableau', 'pandas', 'numpy', 'machine learning',\n  'financial modeling', 'valuation', 'accounting', 'gaap', 'ifrs', 'credit analysis', 'credit risk',\n  'risk management', 'market risk', 'operational risk', 'fraud detection', 'aml', 'kyc', 'compliance',\n  'regulatory reporting', 'loan underwriting', 'loan origination', 'collections', 'reconciliation',\n  'payments', 'upi', 'cards', 'lending', 'retail banking', 'corporate banking', 'treasury',\n  'trade finance', 'wealth management', 'portfolio management', 'investment banking', 'capital markets',\n  'insurance underwriting', 'claims processing', 'actuarial', 'policy servicing', 'salesforce', 'sap',\n  'crm', 'customer onboarding', 'relationship management', 'product management', 'data analysis',\n  'java', 'javascript', 'typescript', 'react', 'node.js', 'aws', 'docker', 'api', 'microservices'\n];\n\nconst ROLE_MAP = {\n  software_engineering: ['software engineer', 'backend engineer', 'frontend engineer', 'full stack', 'developer', 'sde'],\n  data_ai: ['data analyst', 'data scientist', 'ml engineer', 'analytics', 'business analyst', 'bi analyst'],\n  product: ['product manager', 'product analyst', 'program manager'],\n  finance_analytics: ['financial analyst', 'fp&a', 'investment analyst', 'credit analyst', 'treasury analyst'],\n  risk_compliance: ['risk analyst', 'risk manager', 'compliance', 'aml', 'kyc', 'fraud analyst', 'audit'],\n  operations: ['operations', 'back office', 'loan operations', 'payment operations', 'reconciliation'],\n  sales_relationship: ['relationship manager', 'sales', 'business development', 'rm', 'inside sales'],\n  insurance_ops: ['underwriter', 'claims', 'actuarial', 'insurance operations', 'policy servicing']\n};\n\nconst SEGMENT_KEYWORDS = {\n  banking: ['bank', 'banking', 'retail banking', 'corporate banking', 'branch', 'casa', 'loan', 'credit'],\n  financial_services: ['fintech', 'financial services', 'payments', 'lending', 'wealth', 'brokerage', 'nbfc', 'capital markets'],\n  insurance: ['insurance', 'underwriting', 'claims', 'actuarial', 'policy', 'insurtech'],\n};\n\nconst skills = unique([...extractMatches(SKILLS), ...(data.structured_skills || [])]);\nconst roleFamilies = unique(Object.entries(ROLE_MAP)\n  .filter(([, keywords]) => keywords.some((keyword) => lower.includes(keyword)))\n  .map(([family]) => family));\n\nconst segmentScores = Object.fromEntries(\n  Object.entries(SEGMENT_KEYWORDS).map(([segment, keywords]) => {\n    const hits = keywords.filter((keyword) => lower.includes(keyword)).length;\n    return [segment, Math.min(hits * 20, 100)];\n  })\n);\n\nconst years = [...new Set((text.match(/\\b(?:19|20)\\d{2}\\b/g) || []).map(Number))].sort((a, b) => a - b);\nconst yearsExperience = years.length >= 2\n  ? Math.max(0, Math.min(new Date().getFullYear(), years[years.length - 1]) - years[0])\n  : 0;\n\nconst educationSignals = unique((text.match(/\\b(b\\.tech|m\\.tech|mba|bba|bcom|mcom|bsc|msc|ca|cfa|frm|actuarial|finance|economics|commerce|statistics)\\b/gi) || [])\n  .map((item) => item.toLowerCase()));\n\nconst fresher = /\\bfresher\\b|\\bentry level\\b|\\bgraduate\\b|\\bintern\\b|\\bstudent\\b/.test(lower);\n\nreturn [{\n  json: {\n    payload_type: 'candidate_entities',\n    candidate_name: data.candidate_name,\n    clean_text: text,\n    words: data.words,\n    skills,\n    role_families: roleFamilies,\n    segment_scores: segmentScores,\n    years_experience: yearsExperience,\n    education_signals: educationSignals,\n    fresher,\n    location_hints: data.location_hints || [],\n    candidate_preferences: data.candidate_preferences || {},\n    chat_profile: data.chat_profile || {},\n    candidate_profile: data.candidate_profile || {},\n    target_segments: data.target_segments,\n    top_k: data.top_k,\n  },\n}];"
      },
      "id": "3b3ef9a9-e455-4637-a0da-d5468c78019e",
      "name": "4a. Candidate Profile Extractor",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3340,
        384
      ]
    },
    {
      "parameters": {
        "jsCode": "const data = $json;\n\nconst technicalSkills = ['sql', 'python', 'power bi', 'tableau', 'machine learning', 'java', 'javascript', 'typescript', 'react', 'node.js', 'aws', 'api', 'microservices'];\nconst businessSkills = ['financial modeling', 'valuation', 'credit analysis', 'credit risk', 'risk management', 'aml', 'kyc', 'compliance', 'regulatory reporting', 'loan underwriting', 'payments', 'lending', 'wealth management', 'investment banking', 'capital markets', 'insurance underwriting', 'claims processing', 'actuarial', 'relationship management'];\n\nconst technicalDepth = Math.min(100, data.skills.filter((skill) => technicalSkills.includes(skill)).length * 12);\nconst businessDepth = Math.min(100, data.skills.filter((skill) => businessSkills.includes(skill)).length * 10);\nconst segmentValues = Object.values(data.segment_scores || {});\nconst segmentStrength = segmentValues.length ? Math.max(...segmentValues) : 0;\nconst roleCoverage = Math.min(100, (data.role_families || []).length * 20);\nconst experienceContribution = data.fresher ? 45 : Math.min(100, 35 + data.years_experience * 10);\nconst educationBoost = Math.min(20, (data.education_signals || []).length * 5);\n\nconst readinessScore = Math.min(\n  100,\n  Math.round(segmentStrength * 0.3 + businessDepth * 0.25 + technicalDepth * 0.15 + roleCoverage * 0.15 + experienceContribution * 0.15 + educationBoost)\n);\n\nconst bestSegments = Object.entries(data.segment_scores || {})\n  .filter(([, score]) => score > 0)\n  .sort((a, b) => b[1] - a[1])\n  .map(([segment]) => segment)\n  .slice(0, 3);\n\nconst suggestions = [];\nif (!bestSegments.length) suggestions.push('Add BFSI keywords such as lending, payments, risk, compliance, claims, or underwriting.');\nif (!(data.role_families || []).length) suggestions.push('State your target role clearly: analyst, operations, product, risk, sales, or software.');\nif (!data.fresher && data.years_experience < 2) suggestions.push('Show stronger business impact with quantifiable achievements and ownership metrics.');\nif (data.fresher) suggestions.push('Prioritize entry-level analyst, operations, trainee, and associate roles across BFSI companies.');\n\nlet experienceLevel = 'entry';\nif (!data.fresher && data.years_experience >= 6) experienceLevel = 'senior';\nelse if (!data.fresher && data.years_experience >= 3) experienceLevel = 'mid';\n\nreturn [{\n  json: {\n    payload_type: 'candidate_profile',\n    candidate_name: data.candidate_name,\n    skills: data.skills,\n    role_families: data.role_families,\n    segment_scores: data.segment_scores,\n    best_segments: bestSegments,\n    years_experience: data.years_experience,\n    experience_level: experienceLevel,\n    fresher: data.fresher,\n    education_signals: data.education_signals,\n    location_hints: data.location_hints,\n    technical_depth: technicalDepth,\n    business_depth: businessDepth,\n    readiness_score: readinessScore,\n    suggestions,\n    target_segments: data.target_segments,\n    top_k: data.top_k,\n  },\n}];"
      },
      "id": "0db79d11-0d70-4b46-9f51-6ec6bbf7426f",
      "name": "5a. Candidate BFSI Readiness",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3620,
        384
      ]
    },
    {
      "parameters": {
        "jsCode": "const request = $('2. Candidate + Jobs Validator').item.json;\nconst jobs = (request.jobs_raw || []).map((job, index) => ({\n  job_id: String(job.id || job.job_id || job.req_id || job.url || index + 1),\n  title: String(job.title || job.job_title || '').trim(),\n  company: String(job.company || job.company_name || job.organization || '').trim(),\n  location: String(job.location || job.city || job.work_location || '').trim(),\n  description: String(job.description || job.jd || job.summary || job.requirements || '').trim(),\n  skills: Array.isArray(job.skills) ? job.skills.map((skill) => String(skill).trim().toLowerCase()).filter(Boolean) : [],\n  source: String(job.source || job.platform || '').trim() || 'job_feed',\n  apply_url: String(job.apply_url || job.url || job.link || '').trim(),\n  employment_type: String(job.employment_type || job.type || '').trim(),\n  experience_min: Number(job.experience_min ?? job.min_experience ?? job.min_years ?? job.minYears),\n  experience_max: Number(job.experience_max ?? job.max_experience ?? job.max_years ?? job.maxYears),\n  raw: job,\n})).filter((job) => job.title || job.description);\n\nreturn [{\n  json: {\n    payload_type: 'jobs_raw',\n    jobs,\n    target_segments: request.target_segments,\n    top_k: request.top_k,\n  },\n}];"
      },
      "id": "9d99d952-3e1b-4b46-b8f1-cb47ccf72d5c",
      "name": "3b. Jobs Feed Preprocessor",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3060,
        640
      ]
    },
    {
      "parameters": {
        "jsCode": "const data = $json;\nconst targetSegments = data.target_segments || ['banking', 'financial_services', 'insurance'];\n\nconst SEGMENT_KEYWORDS = {\n  banking: ['bank', 'banking', 'retail banking', 'corporate banking', 'branch', 'credit', 'loan', 'trade finance', 'casa', 'treasury'],\n  financial_services: ['fintech', 'financial services', 'payments', 'upi', 'cards', 'lending', 'wealth', 'brokerage', 'nbfc', 'capital markets'],\n  insurance: ['insurance', 'insurtech', 'underwriting', 'claims', 'actuarial', 'policy', 'bancassurance'],\n};\n\nconst ROLE_MAP = {\n  software_engineering: ['software engineer', 'backend', 'frontend', 'full stack', 'developer', 'sde'],\n  data_ai: ['data analyst', 'data scientist', 'analytics', 'business intelligence', 'bi analyst', 'ml engineer'],\n  product: ['product manager', 'product analyst', 'program manager'],\n  finance_analytics: ['financial analyst', 'credit analyst', 'investment analyst', 'treasury analyst'],\n  risk_compliance: ['risk', 'compliance', 'aml', 'kyc', 'fraud', 'audit'],\n  operations: ['operations', 'back office', 'loan operations', 'reconciliation', 'payment operations'],\n  sales_relationship: ['relationship manager', 'sales', 'business development', 'rm'],\n  insurance_ops: ['underwriter', 'claims', 'actuarial', 'policy servicing']\n};\n\nconst SKILLS = [\n  'sql', 'python', 'excel', 'power bi', 'tableau', 'financial modeling', 'valuation', 'credit analysis', 'credit risk',\n  'risk management', 'aml', 'kyc', 'compliance', 'regulatory reporting', 'loan underwriting', 'collections',\n  'reconciliation', 'payments', 'upi', 'cards', 'lending', 'retail banking', 'corporate banking', 'treasury',\n  'trade finance', 'wealth management', 'investment banking', 'capital markets', 'insurance underwriting',\n  'claims processing', 'actuarial', 'relationship management', 'salesforce', 'sap', 'crm', 'java', 'javascript',\n  'typescript', 'react', 'node.js', 'aws', 'api', 'microservices'\n];\n\nconst parseExperience = (job, text) => {\n  const rangeMatch = text.match(/(\\d+)\\s*[-to]{1,3}\\s*(\\d+)\\s*(?:years|yrs)/i);\n  if (rangeMatch) {\n    return { min: Number(rangeMatch[1]), max: Number(rangeMatch[2]) };\n  }\n  const singleMatch = text.match(/(\\d+)\\+?\\s*(?:years|yrs)/i);\n  const min = Number.isFinite(job.experience_min) ? job.experience_min : singleMatch ? Number(singleMatch[1]) : 0;\n  const max = Number.isFinite(job.experience_max) ? job.experience_max : (singleMatch ? Number(singleMatch[1]) + 2 : min + 2);\n  return { min, max };\n};\n\nconst jobs = (data.jobs || []).map((job) => {\n  const text = [job.title, job.company, job.location, job.description, job.skills.join(' ')].filter(Boolean).join(' \\n ').toLowerCase();\n  const segmentScores = Object.fromEntries(\n    Object.entries(SEGMENT_KEYWORDS).map(([segment, keywords]) => {\n      const hits = keywords.filter((keyword) => text.includes(keyword)).length;\n      return [segment, Math.min(hits * 20, 100)];\n    })\n  );\n  const matchedSegments = Object.entries(segmentScores)\n    .filter(([, score]) => score > 0)\n    .sort((a, b) => b[1] - a[1])\n    .map(([segment]) => segment);\n  const primarySegment = matchedSegments[0] || targetSegments[0] || 'financial_services';\n  const roleFamily = Object.entries(ROLE_MAP).find(([, keywords]) => keywords.some((keyword) => text.includes(keyword)))?.[0] || 'operations';\n  const requiredSkills = [...new Set([...job.skills, ...SKILLS.filter((skill) => text.includes(skill))])];\n  const experience = parseExperience(job, text);\n  const isBfsi = matchedSegments.length > 0 || /\\bbank\\b|\\binsurance\\b|\\bfintech\\b|\\bfinancial services\\b|\\bnbfc\\b/.test(text);\n\n  return {\n    ...job,\n    segment_scores: segmentScores,\n    matched_segments: matchedSegments,\n    primary_segment: primarySegment,\n    role_family: roleFamily,\n    required_skills: requiredSkills,\n    experience_range: experience,\n    is_bfsi: isBfsi,\n    normalized_text: text,\n    remote_friendly: /\\bremote\\b|\\bhybrid\\b/.test(text),\n  };\n});\n\nconst bfsiJobs = jobs.filter((job) => job.is_bfsi && targetSegments.includes(job.primary_segment));\nconst finalJobs = bfsiJobs.length ? bfsiJobs : jobs;\n\nreturn [{\n  json: {\n    payload_type: 'job_catalog',\n    jobs: finalJobs.slice(0, 250),\n    total_jobs_received: jobs.length,\n    bfsi_jobs_retained: finalJobs.length,\n    fallback_used: bfsiJobs.length === 0,\n    target_segments: targetSegments,\n    top_k: data.top_k,\n  },\n}];"
      },
      "id": "dd307fe4-1f0f-4893-a7e3-421df6f5ce2b",
      "name": "4b. BFSI Job Normalizer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3340,
        640
      ]
    },
    {
      "parameters": {
        "jsCode": "const request = $('2. Candidate + Jobs Validator').item.json;\nconst preferences = request.candidate_preferences || {};\nconst chatProfile = request.chat_profile || {};\nconst lowerBlob = JSON.stringify({ preferences, chatProfile, text: request.candidate_text || '' }).toLowerCase();\n\nconst normalizeList = (value) => Array.isArray(value)\n  ? value.map((item) => String(item).trim().toLowerCase()).filter(Boolean)\n  : [];\n\nconst preferredLocations = [\n  ...normalizeList(preferences.locations),\n  ...normalizeList(chatProfile.locations),\n].filter(Boolean);\n\nconst preferredSegments = [\n  ...normalizeList(preferences.segments),\n  ...normalizeList(chatProfile.segments),\n  ...(request.target_segments || []),\n].filter(Boolean);\n\nconst preferredRoleFamilies = [\n  ...normalizeList(preferences.role_families),\n  ...normalizeList(chatProfile.role_families),\n].filter(Boolean);\n\nconst remoteOk = Boolean(preferences.remote_ok || chatProfile.remote_ok || /\\bremote\\b|\\bhybrid\\b/.test(lowerBlob));\nconst fresherHint = /\\bfresher\\b|\\bentry level\\b|\\bgraduate\\b|\\bintern\\b/.test(lowerBlob);\n\nconst weights = fresherHint\n  ? { skill: 0.38, segment: 0.24, role: 0.18, experience: 0.08, location: 0.12 }\n  : { skill: 0.32, segment: 0.26, role: 0.16, experience: 0.16, location: 0.10 };\n\nreturn [{\n  json: {\n    payload_type: 'preferences',\n    preferred_locations: [...new Set(preferredLocations)],\n    preferred_segments: [...new Set(preferredSegments)],\n    preferred_role_families: [...new Set(preferredRoleFamilies)],\n    remote_ok: remoteOk,\n    weights,\n    top_k: request.top_k,\n  },\n}];"
      },
      "id": "d2781bd1-acdc-495b-b754-28b7213221d3",
      "name": "3c. Preference Normalizer",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3060,
        896
      ]
    },
    {
      "parameters": {
        "mode": "combine",
        "combineBy": "combineAll",
        "options": {}
      },
      "id": "6f0a0ac8-7a23-4afc-9804-7273056e82a1",
      "name": "7. Merge Matching Inputs",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        3900,
        640
      ]
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst candidate = items.find((item) => item.json.payload_type === 'candidate_profile')?.json;\nconst catalog = items.find((item) => item.json.payload_type === 'job_catalog')?.json;\nconst preferences = items.find((item) => item.json.payload_type === 'preferences')?.json;\n\nif (!candidate) throw new Error('Candidate profile branch did not produce output.');\nif (!catalog) throw new Error('Job catalog branch did not produce output.');\nif (!preferences) throw new Error('Preference branch did not produce output.');\n\nconst roleAdjacency = {\n  software_engineering: ['data_ai', 'product'],\n  data_ai: ['software_engineering', 'finance_analytics', 'risk_compliance'],\n  product: ['software_engineering', 'operations', 'finance_analytics'],\n  finance_analytics: ['risk_compliance', 'data_ai', 'operations'],\n  risk_compliance: ['finance_analytics', 'operations', 'insurance_ops'],\n  operations: ['risk_compliance', 'sales_relationship', 'insurance_ops'],\n  sales_relationship: ['operations', 'finance_analytics'],\n  insurance_ops: ['risk_compliance', 'operations'],\n};\n\nconst overlapScore = (candidateSkills, jobSkills) => {\n  if (!jobSkills.length) return 60;\n  const overlap = jobSkills.filter((skill) => candidateSkills.includes(skill));\n  return Math.round((overlap.length / jobSkills.length) * 100);\n};\n\nconst roleScoreFor = (candidateRoles, jobRole) => {\n  if (!jobRole) return 60;\n  if (candidateRoles.includes(jobRole)) return 100;\n  if (candidateRoles.some((role) => (roleAdjacency[role] || []).includes(jobRole))) return 72;\n  return candidateRoles.length ? 35 : 55;\n};\n\nconst experienceScoreFor = (yearsExperience, job, fresher) => {\n  const min = Math.max(0, Number(job.experience_range?.min || 0));\n  const max = Math.max(min, Number(job.experience_range?.max || min));\n  if (fresher && min <= 1) return 88;\n  if (yearsExperience >= min && yearsExperience <= max + 2) return 100;\n  if (yearsExperience + 1 >= min) return 70;\n  if (yearsExperience === 0 && min <= 2) return 62;\n  return 30;\n};\n\nconst locationScoreFor = (job) => {\n  const prefs = preferences.preferred_locations || [];\n  if (!prefs.length) return job.remote_friendly && preferences.remote_ok ? 95 : 70;\n  const location = (job.location || '').toLowerCase();\n  if (preferences.remote_ok && job.remote_friendly) return 100;\n  if (prefs.some((pref) => location.includes(pref))) return 100;\n  return 45;\n};\n\nconst segmentScoreFor = (job) => {\n  const segmentBase = Number(candidate.segment_scores?.[job.primary_segment] || 0);\n  if (!segmentBase && (preferences.preferred_segments || []).includes(job.primary_segment)) return 70;\n  return Math.max(segmentBase, 35);\n};\n\nconst explanationFor = (job, matchedSkills, missingSkills, scores) => {\n  const reasons = [];\n  reasons.push('Best segment fit: ' + job.primary_segment.replace(/_/g, ' '));\n  if (matchedSkills.length) reasons.push('Matched skills: ' + matchedSkills.slice(0, 5).join(', '));\n  if (candidate.role_families.includes(job.role_family)) reasons.push('Role family aligns with your background in ' + job.role_family.replace(/_/g, ' '));\n  if (scores.experience >= 80) reasons.push('Experience is within or close to the role range');\n  if (scores.location >= 90) reasons.push('Location preference aligns well');\n  if (!reasons.length) reasons.push('General BFSI alignment based on your profile and target sectors');\n  return reasons;\n};\n\nconst allMatches = (catalog.jobs || []).map((job) => {\n  const matchedSkills = (job.required_skills || []).filter((skill) => candidate.skills.includes(skill));\n  const missingSkills = (job.required_skills || []).filter((skill) => !candidate.skills.includes(skill));\n\n  const scores = {\n    skill: overlapScore(candidate.skills || [], job.required_skills || []),\n    segment: segmentScoreFor(job),\n    role: roleScoreFor(candidate.role_families || [], job.role_family),\n    experience: experienceScoreFor(candidate.years_experience || 0, job, candidate.fresher),\n    location: locationScoreFor(job),\n  };\n\n  const weighted =\n    scores.skill * preferences.weights.skill +\n    scores.segment * preferences.weights.segment +\n    scores.role * preferences.weights.role +\n    scores.experience * preferences.weights.experience +\n    scores.location * preferences.weights.location;\n\n  const technicalBoost = ['software_engineering', 'data_ai', 'product'].includes(job.role_family)\n    ? Math.round((candidate.technical_depth || 0) * 0.06)\n    : 0;\n\n  const finalScore = Math.min(100, Math.round(weighted + technicalBoost));\n\n  const recommendation = finalScore >= 82\n    ? 'Strong fit'\n    : finalScore >= 68\n      ? 'Good fit'\n      : finalScore >= 55\n        ? 'Moderate fit'\n        : 'Stretch fit';\n\n  const riskFlags = [];\n  if (scores.experience < 60) riskFlags.push('Experience gap vs stated range');\n  if (missingSkills.length >= 5) riskFlags.push('Several required skills are still missing');\n  if (scores.location < 60) riskFlags.push('Location preference may not align');\n\n  return {\n    job_id: job.job_id,\n    title: job.title,\n    company: job.company || 'Unknown company',\n    location: job.location || 'Not specified',\n    source: job.source,\n    apply_url: job.apply_url,\n    segment: job.primary_segment,\n    role_family: job.role_family,\n    employment_type: job.employment_type,\n    match_score: finalScore,\n    recommendation,\n    score_breakdown: scores,\n    matched_skills: matchedSkills.slice(0, 8),\n    missing_skills: missingSkills.slice(0, 8),\n    why_matched: explanationFor(job, matchedSkills, missingSkills, scores),\n    risk_flags: riskFlags,\n    experience_range: job.experience_range,\n  };\n}).sort((a, b) => b.match_score - a.match_score);\n\nconst topMatches = allMatches.slice(0, preferences.top_k || candidate.top_k || 10);\nconst segmentSummary = topMatches.reduce((acc, job) => {\n  acc[job.segment] = (acc[job.segment] || 0) + 1;\n  return acc;\n}, {});\n\nreturn [{\n  json: {\n    analysis_type: 'bfsi_job_match',\n    pipeline_version: '2026-04-04-bfsi-v1',\n    analysis_id: 'n8n-' + Date.now().toString(36),\n    analyzed_at: new Date().toISOString(),\n    market_focus: ['banking', 'financial_services', 'insurance'],\n    candidate_summary: {\n      name: candidate.candidate_name,\n      readiness_score: candidate.readiness_score,\n      years_experience: candidate.years_experience,\n      experience_level: candidate.experience_level,\n      fresher: candidate.fresher,\n      best_segments: candidate.best_segments,\n      role_families: candidate.role_families,\n      skills: (candidate.skills || []).slice(0, 20),\n      suggestions: candidate.suggestions,\n    },\n    job_market_summary: {\n      jobs_received: catalog.total_jobs_received,\n      jobs_ranked: (catalog.jobs || []).length,\n      bfsi_jobs_retained: catalog.bfsi_jobs_retained,\n      fallback_used: catalog.fallback_used,\n      segment_summary: segmentSummary,\n    },\n    top_matches: topMatches,\n    total_matches_returned: topMatches.length,\n  },\n}];"
      },
      "id": "6a1859e8-3230-4d1b-b423-fbd07bf42323",
      "name": "8. BFSI Match Ranker",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4180,
        640
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify($json) }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Content-Type",
                "value": "application/json"
              },
              {
                "name": "X-Pipeline",
                "value": "QuestGuard-BFSI-Matcher"
              }
            ]
          }
        }
      },
      "id": "8e9fe2f9-3906-446c-8cf8-3f2e58497c96",
      "name": "9. Return Matches",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        4460,
        640
      ]
    }
  ],
  "connections": {
    "1. Webhook Trigger": {
      "main": [
        [
          {
            "node": "2. Candidate + Jobs Validator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Candidate + Jobs Validator": {
      "main": [
        [
          {
            "node": "3a. Candidate Text Preprocessor",
            "type": "main",
            "index": 0
          },
          {
            "node": "3b. Jobs Feed Preprocessor",
            "type": "main",
            "index": 0
          },
          {
            "node": "3c. Preference Normalizer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3a. Candidate Text Preprocessor": {
      "main": [
        [
          {
            "node": "4a. Candidate Profile Extractor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4a. Candidate Profile Extractor": {
      "main": [
        [
          {
            "node": "5a. Candidate BFSI Readiness",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "5a. Candidate BFSI Readiness": {
      "main": [
        [
          {
            "node": "7. Merge Matching Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3b. Jobs Feed Preprocessor": {
      "main": [
        [
          {
            "node": "4b. BFSI Job Normalizer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4b. BFSI Job Normalizer": {
      "main": [
        [
          {
            "node": "7. Merge Matching Inputs",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "3c. Preference Normalizer": {
      "main": [
        [
          {
            "node": "7. Merge Matching Inputs",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "7. Merge Matching Inputs": {
      "main": [
        [
          {
            "node": "8. BFSI Match Ranker",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8. BFSI Match Ranker": {
      "main": [
        [
          {
            "node": "9. Return Matches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {
    "executionOrder": "v1",
    "binaryMode": "separate",
    "availableInMCP": false
  },
  "versionId": "2f7a92f8-4b20-4fb7-bd17-7f72d12ffcf6",
  "id": "2bdnAo59FzmqMUQn",
  "tags": []
}