{
  "nodes": [
    {
      "id": "57beb602-7b80-4a2e-80cf-5fb46dc17858",
      "name": "Main Sticky",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2368,
        2640
      ],
      "parameters": {
        "color": 2,
        "width": 500,
        "height": 600,
        "content": "## Aggregate Multi-Source Job Boards into Centralized Database\nAutomate the collection, parsing, and normalization of job postings from diverse ATS platforms into a unified database.\n\n### How it works\n1. Schedule daily execution using the trigger.\n2. Iterate through defined ATS source configurations.\n3. Fetch raw job data via HTTP requests.\n4. Normalize diverse JSON structures into a standard schema.\n5. Filter by domain and deduplicate entries.\n6. Sync results to Supabase and Google Sheets.\n\n### Setup\n1. Define your job board list in the first Code node.\n2. Add Supabase credentials in the Postgres node.\n3. Configure Google Sheets credentials and document ID.\n4. Update the ALLOWED_DOMAINS array to match your career target.\n\n### Customization\nConsolidate user-specific values like API keys or table IDs in a Set node at the workflow start for easy configuration."
      },
      "typeVersion": 1
    },
    {
      "id": "b5c07db8-536b-4212-8753-f3033ef81b55",
      "name": "Company List",
      "type": "n8n-nodes-base.code",
      "position": [
        -1568,
        2736
      ],
      "parameters": {
        "jsCode": "// COMPANY LIST v7\n// - Board APIs first (no array-splitting)\n// - Per-company ATS (JSON objects, no splitting)\n// - HTML/JSON-LD career pages (test batch)\n// - Lever + RemoteOK LAST (bare array => n8n splits => handled by v7 parser)\n\nconst sources = [\n\n  // FREE GLOBAL JOB BOARDS (safe -- return JSON objects)\n  { ats: 'remotive',  slug: 'remotive',  company: 'Remotive Board' },\n  { ats: 'himalayas', slug: 'himalayas', company: 'Himalayas' },\n  { ats: 'arbeitnow', slug: 'arbeitnow', company: 'Arbeitnow' },\n  { ats: 'jobicy',    slug: 'india',     company: 'Jobicy India' },\n  { ats: 'jobicy',    slug: 'usa',       company: 'Jobicy USA Remote' },\n\n  // GREENHOUSE -- returns {jobs:[]} object, no splitting\n  { ats: 'greenhouse', slug: 'gitlab',       company: 'GitLab' },\n  { ats: 'greenhouse', slug: 'razorpay',     company: 'Razorpay' },\n  { ats: 'greenhouse', slug: 'postman',      company: 'Postman' },\n  { ats: 'greenhouse', slug: 'browserstack', company: 'BrowserStack' },\n  { ats: 'greenhouse', slug: 'moengage',     company: 'MoEngage' },\n  { ats: 'greenhouse', slug: 'hasura',       company: 'Hasura' },\n  { ats: 'greenhouse', slug: 'notion',       company: 'Notion' },\n  { ats: 'greenhouse', slug: 'groww',        company: 'Groww' },\n  { ats: 'greenhouse', slug: 'druva',        company: 'Druva' },\n  { ats: 'greenhouse', slug: 'cloudflare',   company: 'Cloudflare' },\n  { ats: 'greenhouse', slug: 'rippling',     company: 'Rippling' },\n  { ats: 'greenhouse', slug: 'gusto',        company: 'Gusto' },\n  { ats: 'greenhouse', slug: 'twilio',       company: 'Twilio' },\n  { ats: 'greenhouse', slug: 'brex',         company: 'Brex' },\n  { ats: 'greenhouse', slug: 'grammarly',    company: 'Grammarly' },\n  { ats: 'greenhouse', slug: 'zapier',       company: 'Zapier' },\n\n  // SMARTRECRUITERS -- returns {content:[]} object\n  { ats: 'smartrecruiters', slug: 'Meesho',     company: 'Meesho' },\n  { ats: 'smartrecruiters', slug: 'Freshworks', company: 'Freshworks' },\n  { ats: 'smartrecruiters', slug: 'PhonePe',    company: 'PhonePe' },\n\n  // ASHBY -- returns {jobs:[]} object\n  { ats: 'ashby', slug: 'linear',   company: 'Linear' },\n  { ats: 'ashby', slug: 'clerk',    company: 'Clerk.dev' },\n  { ats: 'ashby', slug: 'cal',      company: 'Cal.com' },\n  { ats: 'ashby', slug: 'supabase', company: 'Supabase' },\n  { ats: 'ashby', slug: 'resend',   company: 'Resend' },\n  { ats: 'ashby', slug: 'posthog',  company: 'PostHog' },\n  { ats: 'ashby', slug: 'raycast',  company: 'Raycast' },\n  { ats: 'ashby', slug: 'vercel',   company: 'Vercel' },\n\n  // WORKABLE -- returns {results:[]} object\n  { ats: 'workable', slug: 'zepto',      company: 'Zepto' },\n  { ats: 'workable', slug: 'cred',       company: 'CRED' },\n  { ats: 'workable', slug: 'shiprocket', company: 'Shiprocket' },\n\n  // REPLACED HTML LISTINGS WITH VERIFIED ENDPOINTS\n  { ats: 'greenhouse', slug: 'shopify',   company: 'Shopify' },\n  { ats: 'greenhouse', slug: 'chargebee', company: 'Chargebee' },\n  { ats: 'greenhouse', slug: 'sprinklr',  company: 'Sprinklr' },\n\n  // LEVER -- bare array response => n8n SPLITS items\n  // Placed LAST so index-split only affects entries at the tail.\n  // v7 parser detects company from hostedUrl automatically.\n  { ats: 'lever', slug: 'gohighlevel', company: 'GoHighLevel' },\n  { ats: 'lever', slug: 'remote',      company: 'Remote.com' },\n  { ats: 'lever', slug: 'delhivery',   company: 'Delhivery' },\n  { ats: 'lever', slug: 'netlify',     company: 'Netlify' },\n  { ats: 'lever', slug: 'figma',       company: 'Figma' },\n  { ats: 'lever', slug: 'loom',        company: 'Loom' },\n\n  // REMOTEOK -- bare array response => n8n SPLITS items\n  // Placed last. Parser reads .company field from each split item.\n  { ats: 'remoteok', slug: 'remoteok', company: 'Remote OK' },\n\n];\n\nconsole.log('Company List v7: ' + sources.length + ' sources');\nreturn sources.map(c => ({ json: c }));"
      },
      "typeVersion": 2
    },
    {
      "id": "9bf7162f-6b30-4b72-a2eb-de2b0f50c724",
      "name": "Prepare Request",
      "type": "n8n-nodes-base.code",
      "position": [
        -1056,
        2752
      ],
      "parameters": {
        "jsCode": "// Builds API URL + headers for ALL sources in one pass.\n// Each source gets its own item => HTTP Request calls each one.\n// v2: Adds pagination for Remotive (higher limit), SmartRecruiters (offset),\n//     and Himalayas (page param) to capture all available jobs.\n\nconst H = { 'Accept': 'application/json', 'User-Agent': 'Mozilla/5.0 (compatible; JobBot/4.2)' };\nconst results = [];\n\nfor (const entry of $input.all()) {\n  const { ats, slug, company, url: customUrl } = entry.json;\n  if (!ats || !slug) continue;\n\n  let headers = H;\n  if (ats === 'remoteok') {\n    headers = { ...H, 'Accept': 'application/json', 'Cache-Control': 'no-cache' };\n  } else if (ats === 'html_json_ld') {\n    headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Accept': 'text/html' };\n  }\n  const route = ats === 'html_json_ld' ? 'html' : 'api';\n  const base  = { ...entry.json, _headers: headers, _route: route };\n\n  // Paginated sources\n  // SmartRecruiters: offset-based pagination (5 pages x 100 = 500 jobs max)\n  if (ats === 'smartrecruiters') {\n    for (let offset = 0; offset < 500; offset += 100) {\n      results.push({ json: { ...base, _url: 'https://api.smartrecruiters.com/v1/companies/' + slug + '/postings?status=PUBLIC&limit=100&offset=' + offset } });\n    }\n    continue;\n  }\n\n  // Himalayas: page-based pagination (10 pages x 50 = 500 jobs max)\n  if (ats === 'himalayas') {\n    for (let page = 1; page <= 10; page++) {\n      results.push({ json: { ...base, _url: 'https://himalayas.app/jobs/api?limit=50&page=' + page } });\n    }\n    continue;\n  }\n\n  // Non-paginated sources (single URL)\n  const urlMap = {\n    remotive:     'https://remotive.com/api/remote-jobs?limit=500',\n    remoteok:     'https://remoteok.com/api',\n    arbeitnow:    'https://www.arbeitnow.com/api/job-board-api',\n    jobicy:       slug === 'india'\n                    ? 'https://jobicy.com/api/v2/remote-jobs?count=100&geo=india'\n                    : 'https://jobicy.com/api/v2/remote-jobs?count=100&geo=usa',\n    lever:        'https://api.lever.co/v0/postings/' + slug + '?mode=json&limit=250',\n    greenhouse:   'https://boards-api.greenhouse.io/v1/boards/' + slug + '/jobs?content=true',\n    workable:     'https://apply.workable.com/api/v1/widget/accounts/' + slug,\n    ashby:        'https://api.ashbyhq.com/posting-api/job-board/' + slug,\n    html_json_ld: customUrl\n  };\n\n  const url = urlMap[ats];\n  if (!url) { console.log('Unknown ATS: ' + ats); continue; }\n\n  results.push({ json: { ...base, _url: url } });\n}\n\nconsole.log('Prepare Request v2: ' + results.length + ' API calls queued (includes pagination)');\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "be38d26b-6c7f-4929-9968-554af409ccbd",
      "name": "HTTP Request",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        -832,
        2752
      ],
      "parameters": {
        "url": "={{ $json._url }}",
        "options": {
          "timeout": 30000,
          "batching": {
            "batch": {
              "batchSize": 5
            }
          }
        },
        "jsonHeaders": "={{ JSON.stringify($json._headers) }}",
        "sendHeaders": true,
        "specifyHeaders": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "339dcf49-a2de-4f81-af07-05baf1672f0f",
      "name": "Upsert to Supabase",
      "type": "n8n-nodes-base.postgres",
      "onError": "continueRegularOutput",
      "position": [
        -272,
        2752
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "name",
          "value": "YOUR_TABLE_NAME"
        },
        "schema": {
          "__rl": true,
          "mode": "name",
          "value": "public"
        },
        "columns": {
          "value": {
            "salary": "={{ $json.salary }}",
            "status": "={{ $json.status }}",
            "company": "={{ $json.company }}",
            "country": "={{ $json.country }}",
            "ats_type": "={{ $json.ats_type }}",
            "job_hash": "={{ $json.job_hash }}",
            "location": "={{ $json.location }}",
            "apply_url": "={{ $json.apply_url }}",
            "job_title": "={{ $json.job_title }}",
            "work_mode": "={{ $json.work_mode }}",
            "description": "={{ $json.description }}",
            "employment_type": "={{ $json.employment_type }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "number",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "job_title",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "job_title",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "company",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "company",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "location",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "location",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "country",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "country",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "work_mode",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "work_mode",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "employment_type",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "employment_type",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "apply_url",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "apply_url",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "ats_type",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ats_type",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "job_hash",
              "type": "string",
              "display": true,
              "required": true,
              "displayName": "job_hash",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "description",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "description",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "salary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "salary",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "scraped_at",
              "type": "dateTime",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "scraped_at",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            },
            {
              "id": "created_at",
              "type": "dateTime",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "created_at",
              "defaultMatch": false,
              "canBeUsedToMatch": false
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "job_hash"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "upsert"
      },
      "typeVersion": 2.5
    },
    {
      "id": "801e7955-bf21-4695-a591-cc716267e1e6",
      "name": "Write to Google Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -16,
        2752
      ],
      "parameters": {
        "columns": {
          "value": {
            "ATS": "={{ $('Parse and Enrich and Filter').item.json.ats_type }}",
            "Salary": "={{ $('Parse and Enrich and Filter').item.json.salary }}",
            "Status": "={{ $('Parse and Enrich and Filter').item.json.status }}",
            "Company": "={{ $('Parse and Enrich and Filter').item.json.company }}",
            "Country": "={{ $('Parse and Enrich and Filter').item.json.country }}",
            "Location": "={{ $('Parse and Enrich and Filter').item.json.location }}",
            "job_hash": "={{ $('Parse and Enrich and Filter').item.json.job_hash }}",
            "Apply URL": "={{ $('Parse and Enrich and Filter').item.json.apply_url }}",
            "Job Title": "={{ $('Parse and Enrich and Filter').item.json.job_title }}",
            "Work Mode": "={{ $('Parse and Enrich and Filter').item.json.work_mode }}",
            "Description": "={{ $('Parse and Enrich and Filter').item.json.description }}",
            "Employment Type": "={{ $('Parse and Enrich and Filter').item.json.employment_type }}"
          },
          "schema": [
            {
              "id": "job_hash",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "job_hash",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Job Title",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Job Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Company",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Company",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Location",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Location",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Country",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Country",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Work Mode",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Work Mode",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Employment Type",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Employment Type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Apply URL",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Apply URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ATS",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ATS",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Salary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Salary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Description",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Description",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "job_hash"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_RESOURCE_ID_HERE"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_RESOURCE_ID_HERE"
        }
      },
      "typeVersion": 4
    },
    {
      "id": "dcda27e6-1c40-419e-8351-4bc8a8547d7d",
      "name": "Loop Batches (5)",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -1344,
        2736
      ],
      "parameters": {
        "options": {},
        "batchSize": 5
      },
      "typeVersion": 3
    },
    {
      "id": "9f6b95c3-b316-4f9d-ba64-463737d5a672",
      "name": "Daily 8AM IST",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1792,
        2736
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "b367197d-eb1e-468e-8dd2-08173782b16a",
      "name": "Parse and Enrich and Filter",
      "type": "n8n-nodes-base.code",
      "position": [
        -592,
        2752
      ],
      "parameters": {
        "jsCode": "// PARSE + ENRICH + FILTER\n\n// tiny helpers\nconst str = v => {\n  if (v == null) return '';\n  if (typeof v === 'object') return v.label || v.name || v.id || '';\n  return String(v);\n};\n\n// ISO country map\nconst ISO2 = {IN:'India',US:'United States',GB:'United Kingdom',AU:'Australia',CA:'Canada',DE:'Germany',SG:'Singapore',AE:'UAE',FR:'France',NL:'Netherlands',PK:'Pakistan',PH:'Philippines',NG:'Nigeria',KE:'Kenya',ZA:'South Africa',BR:'Brazil',JP:'Japan',KR:'South Korea',IE:'Ireland',PL:'Poland',NZ:'New Zealand',PT:'Portugal',IT:'Italy',ES:'Spain',UA:'Ukraine'};\nconst CNAMES = {india:'India',bharat:'India','united states':'United States',usa:'United States','united kingdom':'United Kingdom',australia:'Australia',canada:'Canada',germany:'Germany',singapore:'Singapore',uae:'UAE',dubai:'UAE',netherlands:'Netherlands',france:'France',pakistan:'Pakistan',philippines:'Philippines',nigeria:'Nigeria',kenya:'Kenya','south africa':'South Africa',brazil:'Brazil',japan:'Japan','south korea':'South Korea',ireland:'Ireland',poland:'Poland','new zealand':'New Zealand',portugal:'Portugal',italy:'Italy',spain:'Spain',ukraine:'Ukraine'};\nconst INDIA_CITIES = new Set(['bengaluru','bangalore','mumbai','bombay','delhi','new delhi','hyderabad','pune','chennai','madras','kolkata','calcutta','noida','gurugram','gurgaon','ahmedabad','jaipur','kochi','chandigarh','coimbatore','indore','nagpur','bhopal','lucknow','surat','vadodara','trivandrum','vizag','mangalore','mysuru','nashik']);\n\nfunction parseSalary(raw) {\n  if (!raw && raw !== 0) return '';\n  if (typeof raw === 'string') {\n    const s = raw.trim();\n    if (!s || s === '0' || s === 'null') return '';\n    if (/^\\d/.test(s)) {\n      const nums = s.match(/\\d+(?:[,.]\\d+)*/g);\n      if (nums && nums.length >= 2) {\n        const [lo, hi] = nums.map(n => Number(n.replace(/,/g,'')));\n        const sym = /INR/.test(s) ? 'INR ' : /GBP/.test(s) ? 'GBP ' : /EUR/.test(s) ? 'EUR ' : '$';\n        const per = /year|annual/i.test(s) ? '/yr' : /month/i.test(s) ? '/mo' : /hour/i.test(s) ? '/hr' : '';\n        return sym + fmt(lo) + '--' + fmt(hi) + per;\n      }\n    }\n    return s.slice(0, 200);\n  }\n  if (typeof raw === 'number') {\n    return raw > 0 ? '$' + fmt(raw) : '';\n  }\n  if (typeof raw === 'object') {\n    const inner = raw.value || raw;\n    let min = inner.minValue || inner.min || inner.minimum || inner.floor || 0;\n    let max = inner.maxValue || inner.max || inner.maximum || inner.ceiling || 0;\n    const currency = str(raw.currency || inner.currency || raw.salaryCurrency || 'USD');\n    const interval = str(raw.unitText || inner.unitText || inner.interval || raw.salaryFrequency || '');\n    const sym = currency === 'INR' ? 'INR ' : currency === 'GBP' ? 'GBP ' : currency === 'EUR' ? 'EUR ' : '$';\n    const perStr = interval ? ' /' + interval.toLowerCase().replace(/^per\\s*/i,'').replace('year','yr').replace('month','mo').replace('hour','hr') : '';\n    if (!min && !max) return '';\n    const parts = [];\n    if (min) parts.push(sym + fmt(min));\n    if (max && max !== min) parts.push(sym + fmt(max));\n    return parts.join('--') + perStr + ' ' + currency;\n  }\n  return '';\n}\n\nfunction fmt(n) {\n  n = Number(n);\n  if (n >= 10000000) return (n/10000000).toFixed(1).replace(/\\.0$/,'') + 'Cr';\n  if (n >= 100000)   return (n/100000).toFixed(1).replace(/\\.0$/,'') + 'L';\n  if (n >= 1000)     return (n/1000).toFixed(0) + 'k';\n  return String(Math.round(n));\n}\n\nfunction parseDate(raw) {\n  if (!raw) return '';\n  let d;\n  if (typeof raw === 'number') {\n    d = new Date(raw > 1e10 ? raw : raw * 1000);\n  } else if (typeof raw === 'string') {\n    const s = raw.trim();\n    if (!s) return '';\n    d = new Date(s);\n  } else if (raw instanceof Date) {\n    d = raw;\n  }\n  if (!d || isNaN(d.getTime())) return '';\n  return d.toISOString().slice(0, 10);\n}\n\nfunction getCountry(rawLoc, rawCC) {\n  const cc = str(rawCC);\n  if (cc) {\n    const up = cc.trim().toUpperCase();\n    if (ISO2[up]) return ISO2[up];\n    const lo = cc.trim().toLowerCase();\n    if (CNAMES[lo]) return CNAMES[lo];\n    if (cc.trim().length > 2) return cc.trim();\n  }\n  const loc = str(rawLoc);\n  if (!loc) return 'Unknown';\n  const l = loc.toLowerCase();\n  if (/^(remote|remote worldwide|anywhere|global|worldwide)$/i.test(l.trim())) return 'Global';\n  if ([...INDIA_CITIES].some(c => l.includes(c))) return 'India';\n  if (/\\busa\\b|united states/.test(l)) return 'United States';\n  for (const [a,c] of Object.entries(CNAMES)) if (l.includes(a)) return c;\n  const last = loc.split(/[,|\\/]/).pop()?.trim().toUpperCase();\n  if (last && ISO2[last]) return ISO2[last];\n  return 'Unknown';\n}\n\nfunction getWorkMode(title, rawLoc, explicit) {\n  const ex = str(explicit).toLowerCase().replace(/[^a-z]/g,'');\n  if (ex === 'remote')  return 'Remote';\n  if (ex === 'hybrid')  return 'Hybrid';\n  if (ex === 'onsite' || ex === 'inoffice') return 'Onsite';\n  const t = (str(title) + ' ' + str(rawLoc)).toLowerCase();\n  if (/\\bremote\\b|\\bwfh\\b|work from home|distributed|\\banywhere\\b/.test(t)) return 'Remote';\n  if (/\\bhybrid\\b/.test(t)) return 'Hybrid';\n  return 'Onsite';\n}\n\nfunction cleanLoc(rawLoc, mode) {\n  const loc = str(rawLoc);\n  if (!loc) return mode === 'Remote' ? 'Remote' : 'Not specified';\n  const FIX = {bangalore:'Bengaluru',bombay:'Mumbai',madras:'Chennai',calcutta:'Kolkata',gurgaon:'Gurugram',cochin:'Kochi'};\n  let l = loc.replace(/[,\\s,]+/g, ', ').trim();\n  for (const [o,n] of Object.entries(FIX)) l = l.replace(new RegExp('\\\\b'+o+'\\\\b','gi'), n);\n  return l || 'Not specified';\n}\n\nfunction normType(raw) {\n  const t = str(raw).toLowerCase().replace(/[^a-z]/g,'');\n  if (/intern/.test(t))              return 'Internship';\n  if (/contract|contractor|freelance/.test(t)) return 'Contract';\n  if (/parttime/.test(t))            return 'Part-time';\n  return 'Full-time';\n}\n\nfunction cleanDesc(html) {\n  const h = str(html);\n  if (!h) return '';\n  return h\n    .replace(/<br\\s*\\/?>/gi, '\\n')\n    .replace(/<\\/?(p|li|div|h[1-6])[^>]*>/gi, '\\n')\n    .replace(/<[^>]+>/g, '')\n    .replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&nbsp;/g,' ').replace(/&#\\d+;/g,'')\n    .replace(/\\n{3,}/g, '\\n\\n').trim().slice(0, 2000);\n}\n\nfunction makeHash(ats, company, title, location) {\n  const s = [ats, company, title, location].map(x => (x||'').toLowerCase().trim()).join('|');\n  let h1 = 0, h2 = 0;\n  for (let i = 0; i < s.length; i++) {\n    h1 = Math.imul(31, h1) + s.charCodeAt(i) | 0;\n    h2 = Math.imul(37, h2) + s.charCodeAt(i) | 0;\n  }\n  return Math.abs(h1).toString(36) + Math.abs(h2).toString(36);\n}\n\nconst ALLOWED_DOMAINS = [\n  'business analyst', 'business analysis', 'business systems analyst',\n  'product manager', 'product management', 'associate pm', 'senior pm',\n  'head of product', 'vp of product', 'director of product',\n  'data analyst', 'data analysis', 'analytics analyst',\n  'business intelligence', 'bi analyst', 'reporting analyst',\n  'product designer', 'ux designer', 'ui designer', 'ux/ui', 'ui/ux',\n  'product design', 'interaction designer', 'visual designer',\n  'user experience', 'user interface designer',\n  'full stack', 'fullstack', 'full-stack', 'software engineer',\n  'software developer', 'backend engineer', 'frontend engineer',\n  'backend developer', 'frontend developer', 'web developer',\n  'sales', 'account executive', 'account manager',\n  'business development', 'sdr', 'bdr',\n  'sales development', 'sales manager', 'revenue'\n];\n\nfunction isTitleAllowed(title) {\n  const t = title.toLowerCase();\n  return ALLOWED_DOMAINS.some(kw => t.includes(kw));\n}\n\nfunction shouldKeep(country, work_mode) {\n  if (country === 'India') return true;\n  if (work_mode === 'Remote') return true;\n  return false;\n}\n\nfunction buildOutput(rawJobs, ats, company) {\n  const seen = new Set(), out = [];\n  for (const j of rawJobs) {\n    if (!j.title || j.title.length < 2) continue;\n    if (!isTitleAllowed(j.title)) continue;\n    const work_mode       = getWorkMode(j.title, j.rawLoc, j.mode);\n    const country         = getCountry(j.rawLoc, j.rawCC);\n    if (!shouldKeep(country, work_mode)) continue;\n    const location        = cleanLoc(j.rawLoc, work_mode);\n    const employment_type = normType(j.type);\n    const job_hash        = makeHash(ats, company, j.title, location);\n    if (seen.has(job_hash)) continue;\n    seen.add(job_hash);\n    const salaryStr  = parseSalary(j.salary);\n    const postedDate = parseDate(j.posted);\n    out.push({ json: {\n      job_title:       j.title.trim(),\n      company,\n      location,\n      country,\n      work_mode,\n      employment_type,\n      apply_url:       str(j.url),\n      ats_type:        ats,\n      job_hash,\n      description:     cleanDesc(j.desc),\n      salary:          salaryStr,\n      posted_date:     postedDate,\n      status:          'active'\n    }});\n  }\n  return out;\n}\n\nfunction detectATS(resp) {\n  if (typeof resp === 'string' && resp.includes('<html')) return 'html_json_ld';\n  if (resp && resp.data && typeof resp.data === 'string' && resp.data.includes('<html')) return 'html_json_ld';\n  if (!resp || typeof resp !== 'object') return null;\n  if ('text' in resp && 'hostedUrl' in resp && String(resp.hostedUrl).includes('lever.co')) return 'lever';\n  if ('position' in resp && ('apply_url' in resp || 'url' in resp) && 'company' in resp) return 'remoteok';\n  if (Array.isArray(resp)) {\n    const f = resp.find(j => j && typeof j === 'object');\n    if (!f) return null;\n    if ('position' in f && 'company' in f) return 'remoteok';\n    if ('text' in f && 'categories' in f && 'hostedUrl' in f) return 'lever';\n    return null;\n  }\n  if (resp.jobs && Array.isArray(resp.jobs)) {\n    const f = resp.jobs[0] || {};\n    if ('candidate_required_location' in f) return 'remotive';\n    if ('jobTitle' in f) return 'jobicy';\n    if ('absolute_url' in f) return 'greenhouse';\n    if ('jobUrl' in f || 'workplaceType' in f) return 'ashby';\n    return 'ashby';\n  }\n  if (resp.data && Array.isArray(resp.data))    return 'arbeitnow';\n  if (resp.content && Array.isArray(resp.content)) return 'smartrecruiters';\n  if (resp.results && Array.isArray(resp.results)) return 'workable';\n  return null;\n}\n\nfunction getCompanyFromResp(resp, ats, cfgCompany, cfgSlug) {\n  if (ats === 'remotive')  return 'Remotive Board';\n  if (ats === 'himalayas') return 'Himalayas';\n  if (ats === 'arbeitnow') return 'Arbeitnow';\n  if (ats === 'jobicy')    return cfgSlug === 'india' ? 'Jobicy India' : 'Jobicy USA Remote';\n  if (ats === 'remoteok') {\n    const src = Array.isArray(resp) ? resp[0] : resp;\n    const co  = src && src.company;\n    return co && co !== '0' && co.length > 1 ? co : 'Remote OK';\n  }\n  if (ats === 'lever') {\n    const src = Array.isArray(resp) ? resp[0] : resp;\n    if (src && src.hostedUrl) {\n      const m = src.hostedUrl.match(/lever\\.co\\/([^/?#]+)/);\n      if (m) return m[1].replace(/-/g,' ').replace(/\\b\\w/g, c => c.toUpperCase());\n    }\n    return cfgCompany || cfgSlug || 'Lever';\n  }\n  if (ats === 'greenhouse') {\n    const f = resp.jobs && resp.jobs[0];\n    if (f && f.company_name && f.company_name.length > 1) return f.company_name;\n    if (f && f.absolute_url) { const m = f.absolute_url.match(/greenhouse\\.io\\/([^/?#]+)/); if (m) return m[1]; }\n    return cfgCompany || cfgSlug || 'Greenhouse';\n  }\n  if (ats === 'ashby') {\n    const f = resp.jobs && resp.jobs[0];\n    if (f && f.jobUrl) { const m = f.jobUrl.match(/ashbyhq\\.com\\/([^/?#]+)/); if (m) return m[1].replace(/-/g,' ').replace(/\\b\\w/g, c => c.toUpperCase()); }\n    return cfgCompany || cfgSlug || 'Ashby';\n  }\n  return cfgCompany || cfgSlug || ats;\n}\n\n// Main Loop\nconst httpItems = $input.all();\nconst cfgAll    = $('Prepare Request').all();\nconst allJobs   = [];\n\nconsole.log('Parse v10: ' + httpItems.length + ' HTTP items | ' + cfgAll.length + ' cfg entries');\n\nfor (let i = 0; i < httpItems.length; i++) {\n  const resp = httpItems[i].json;\n\n  let cfgIndex = Math.min(i, cfgAll.length - 1);\n  const paired = httpItems[i].pairedItem;\n  if (paired != null) {\n    if (typeof paired === 'object' && !Array.isArray(paired) && typeof paired.item === 'number') {\n      cfgIndex = Math.min(paired.item, cfgAll.length - 1);\n    } else if (Array.isArray(paired) && paired.length > 0 && typeof paired[0].item === 'number') {\n      cfgIndex = Math.min(paired[0].item, cfgAll.length - 1);\n    }\n  }\n  const cfg = cfgAll[cfgIndex] ? cfgAll[cfgIndex].json : {};\n\n  const ats      = detectATS(resp) || str(cfg.ats);\n  const cfgSlug  = str(cfg.slug);\n  const company  = getCompanyFromResp(resp, ats, str(cfg.company), cfgSlug);\n  const htmlStr  = (typeof resp === 'string') ? resp : (resp && typeof resp.data === 'string' ? resp.data : null);\n\n  if (!ats) { console.log('item ' + i + ': cannot detect ATS'); continue; }\n\n  let rawJobs = [];\n\n  switch (ats) {\n\n    case 'html_json_ld': {\n      if (!htmlStr) { console.log('[html_json_ld] ' + company + ': no HTML body'); break; }\n      let matchCount = 0;\n      const regex = /<script[^>]*type=[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi;\n      let m;\n      while ((m = regex.exec(htmlStr)) !== null) {\n        try {\n          const parsed = JSON.parse(m[1]);\n          const items  = Array.isArray(parsed) ? parsed : [parsed];\n          for (const item of items) {\n            const entities = item['@graph'] || [item];\n            for (const ent of entities) {\n              if (ent['@type'] !== 'JobPosting') continue;\n              matchCount++;\n              const rawLocObj = ent.jobLocation\n                ? (Array.isArray(ent.jobLocation) ? ent.jobLocation[0] : ent.jobLocation)\n                : null;\n              let locStr = 'Remote';\n              let rawCC  = '';\n              if (rawLocObj && rawLocObj.address) {\n                locStr = str(rawLocObj.address.addressLocality)\n                      || str(rawLocObj.address.addressRegion)\n                      || str(rawLocObj.address.addressCountry)\n                      || 'Remote';\n                rawCC  = str(rawLocObj.address.addressCountry);\n              }\n              const isRemote = str(ent.jobLocationType).toLowerCase().includes('remote')\n                            || str(ent.description).toLowerCase().includes('remote');\n              let salary = '';\n              if (ent.baseSalary) {\n                salary = parseSalary(ent.baseSalary);\n              } else if (ent.estimatedSalary) {\n                salary = parseSalary(ent.estimatedSalary);\n              }\n              const posted = parseDate(ent.datePosted || ent.dateCreated || '');\n              rawJobs.push({\n                title:  str(ent.title || ent.name).trim(),\n                rawLoc: isRemote ? 'Remote' : locStr,\n                rawCC,\n                type:   str(ent.employmentType) || 'Full-time',\n                mode:   isRemote ? 'Remote' : null,\n                url:    str(ent.url) || str(cfg.url) || '',\n                desc:   str(ent.description),\n                salary,\n                posted\n              });\n            }\n          }\n        } catch(e) { /* malformed JSON-LD */ }\n      }\n      console.log('[html_json_ld] ' + company + ': ' + matchCount + ' JobPosting entries');\n      break;\n    }\n\n    case 'remotive':\n      rawJobs = (resp.jobs || []).map(j => ({\n        title:  str(j.title).trim(),\n        rawLoc: str(j.candidate_required_location),\n        rawCC:  '',\n        type:   str(j.job_type),\n        mode:   null,\n        url:    str(j.url),\n        desc:   str(j.description),\n        salary: parseSalary(j.salary),\n        posted: parseDate(j.publication_date)\n      }));\n      break;\n\n    case 'remoteok': {\n      const list = Array.isArray(resp)\n        ? resp.filter(j => j && j.position)\n        : (resp && resp.position ? [resp] : []);\n      rawJobs = list.map(j => ({\n        title:  str(j.position).trim(),\n        rawLoc: str(j.location) || 'Worldwide',\n        rawCC:  '',\n        type:   'Full-time',\n        mode:   'Remote',\n        url:    str(j.apply_url) || str(j.url),\n        desc:   str(j.description),\n        salary: (j.salary_min || j.salary_max)\n                  ? parseSalary({ min: j.salary_min, max: j.salary_max, currency: 'USD', interval: 'YEAR' })\n                  : '',\n        posted: parseDate(j.date || j.epoch)\n      }));\n      break;\n    }\n\n    case 'himalayas': {\n      const jobs = resp.jobs || resp.data || (Array.isArray(resp) ? resp : []);\n      rawJobs = jobs.map(j => ({\n        title:  str(j.title).trim(),\n        rawLoc: str(j.location) || str(j.regions) || 'Remote',\n        rawCC:  '',\n        type:   str(j.jobType) || str(j.type),\n        mode:   'Remote',\n        url:    str(j.applyUrl) || str(j.url),\n        desc:   str(j.description),\n        salary: j.salary\n                  ? parseSalary(j.salary)\n                  : (j.minSalary || j.maxSalary)\n                    ? parseSalary({ min: j.minSalary, max: j.maxSalary, currency: j.currency || 'USD', interval: 'YEAR' })\n                    : '',\n        posted: parseDate(j.createdAt || j.publishedAt || j.postedAt)\n      }));\n      break;\n    }\n\n    case 'arbeitnow':\n      rawJobs = (resp.data || []).map(j => ({\n        title:  str(j.title).trim(),\n        rawLoc: str(j.location),\n        rawCC:  '',\n        type:   Array.isArray(j.job_types) ? str(j.job_types[0]) : 'Full-time',\n        mode:   j.remote ? 'Remote' : null,\n        url:    str(j.url),\n        desc:   str(j.description),\n        salary: parseSalary(j.salary || ''),\n        posted: parseDate(j.created_at || j.published_at)\n      }));\n      break;\n\n    case 'jobicy':\n      rawJobs = (resp.jobs || []).map(j => ({\n        title:  str(j.jobTitle).trim(),\n        rawLoc: str(j.jobGeo) || str(j.jobRegion) || 'Remote',\n        rawCC:  '',\n        type:   str(j.jobType),\n        mode:   'Remote',\n        url:    str(j.url),\n        desc:   str(j.jobExcerpt),\n        salary: (j.annualSalaryMin || j.annualSalaryMax)\n                  ? parseSalary({ min: j.annualSalaryMin, max: j.annualSalaryMax, currency: j.salaryCurrency || 'USD', interval: 'YEAR' })\n                  : '',\n        posted: parseDate(j.pubDate || j.postDate)\n      }));\n      break;\n\n    case 'lever': {\n      const list = Array.isArray(resp) ? resp : (resp && resp.text ? [resp] : []);\n      rawJobs = list.map(j => ({\n        title:  str(j.text).trim(),\n        rawLoc: str(j.categories && j.categories.location),\n        rawCC:  '',\n        type:   str(j.categories && j.categories.commitment),\n        mode:   null,\n        url:    str(j.hostedUrl) || str(j.applyUrl),\n        desc:   str(j.descriptionPlain),\n        salary: (j.salaryRange && (j.salaryRange.min || j.salaryRange.max))\n                  ? parseSalary({\n                      min:      j.salaryRange.min,\n                      max:      j.salaryRange.max,\n                      currency: j.salaryRange.currency || 'USD',\n                      interval: j.salaryRange.interval || 'YEAR'\n                    })\n                  : '',\n        posted: parseDate(j.createdAt)\n      }));\n      break;\n    }\n\n    case 'greenhouse':\n      rawJobs = (resp.jobs || []).map(j => ({\n        title:  str(j.title).trim(),\n        rawLoc: str(j.location && j.location.name),\n        rawCC:  '',\n        type:   '',\n        mode:   null,\n        url:    str(j.absolute_url) || ('https://boards.greenhouse.io/' + cfgSlug),\n        desc:   str(j.content),\n        salary: '',\n        posted: parseDate(j.updated_at || j.created_at)\n      }));\n      break;\n\n    case 'workable':\n      rawJobs = (resp.results || []).map(j => {\n        const loc = j.location || {};\n        const city = str(loc.city), cc = str(loc.country_code) || str(loc.country);\n        const rem  = loc.telecommuting || j.remote || false;\n        return {\n          title:  str(j.title).trim(),\n          rawLoc: rem ? ('Remote' + (city ? ', '+city : '')) : [city, cc].filter(Boolean).join(', '),\n          rawCC:  cc,\n          type:   str(j.type),\n          mode:   rem ? 'Remote' : null,\n          url:    str(j.url) || ('https://apply.workable.com/' + cfgSlug),\n          desc:   str(j.description),\n          salary: '',\n          posted: parseDate(j.published_on || j.created_at)\n        };\n      });\n      break;\n\n    case 'smartrecruiters':\n      rawJobs = (resp.content || []).map(j => {\n        const loc  = j.location || {};\n        const city = str(loc.city), cc = str(loc.country);\n        const rem  = loc.remote || false;\n        const comp = j.compensation;\n        return {\n          title:  str(j.name).trim(),\n          rawLoc: rem ? ('Remote' + (city ? ', '+city : '')) : [city, cc].filter(Boolean).join(', '),\n          rawCC:  cc,\n          type:   str(j.typeOfEmployment && j.typeOfEmployment.label),\n          mode:   rem ? 'Remote' : null,\n          url:    str(j.ref) || ('https://careers.smartrecruiters.com/' + cfgSlug),\n          desc:   str(j.jobAdText),\n          salary: comp\n                    ? parseSalary({ min: comp.min, max: comp.max, currency: comp.currency, interval: comp.remuneration })\n                    : '',\n          posted: parseDate(j.releasedDate || j.createdOn)\n        };\n      });\n      break;\n\n    case 'ashby':\n      rawJobs = (resp.jobs || []).map(j => {\n        const addr = (j.address && j.address.postalAddress) || {};\n        return {\n          title:  str(j.title).trim(),\n          rawLoc: str(j.location) || [str(addr.addressLocality), str(addr.addressRegion), str(addr.addressCountry)].filter(Boolean).join(', '),\n          rawCC:  str(addr.addressCountry),\n          type:   str(j.employmentType),\n          mode:   str(j.workplaceType) || (j.isRemote ? 'Remote' : null),\n          url:    str(j.jobUrl) || str(j.applyUrl) || ('https://jobs.ashbyhq.com/' + cfgSlug),\n          desc:   str(j.descriptionHtml) || str(j.descriptionPlain),\n          salary: j.compensationTierSummary\n                    ? parseSalary(str(j.compensationTierSummary))\n                    : (j.salary ? parseSalary(j.salary) : ''),\n          posted: parseDate(j.publishedDate || j.createdAt)\n        };\n      });\n      break;\n\n    default:\n      console.log('Unknown ATS: ' + ats);\n      continue;\n  }\n\n  console.log('[' + ats + '] ' + company + ': ' + rawJobs.length + ' raw jobs');\n  const kept = buildOutput(rawJobs, ats, company);\n  console.log('[' + ats + '] ' + company + ': ' + kept.length + ' kept after filter');\n  for (const job of kept) allJobs.push(job);\n}\n\nconsole.log('Parse v10 total: ' + allJobs.length + ' jobs');\nreturn allJobs.length > 0 ? allJobs : [{ json: { _empty: true } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "05459557-5a0e-452f-a4a5-0b54b9dbb22a",
      "name": "Trigger and Config",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1824,
        2640
      ],
      "parameters": {
        "color": 7,
        "width": 668,
        "height": 280,
        "content": "## 1. Source Discovery and Trigger\nDefines target ATS sources and schedules the daily sync process."
      },
      "typeVersion": 1
    },
    {
      "id": "2931956b-1e57-4945-8209-033742390573",
      "name": "Parsing and Enrichment",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1104,
        2640
      ],
      "parameters": {
        "color": 7,
        "width": 684,
        "height": 280,
        "content": "## 2. Request and Normalization\nExecutes HTTP requests and uses advanced logic to map inconsistent ATS payloads to a standard schema."
      },
      "typeVersion": 1
    },
    {
      "id": "feb79c94-0130-47ac-ae6b-9b70d65c15c5",
      "name": "Destination",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -384,
        2640
      ],
      "parameters": {
        "color": 7,
        "width": 600,
        "height": 280,
        "content": "## 3. Storage and Output\nUpserts standardized job data into Supabase and updates the target Google Sheet."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Company List": {
      "main": [
        [
          {
            "node": "Loop Batches (5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        [
          {
            "node": "Parse and Enrich and Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily 8AM IST": {
      "main": [
        [
          {
            "node": "Company List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Request": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Batches (5)": {
      "main": [
        [],
        [
          {
            "node": "Prepare Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upsert to Supabase": {
      "main": [
        [
          {
            "node": "Write to Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write to Google Sheet": {
      "main": [
        [
          {
            "node": "Loop Batches (5)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse and Enrich and Filter": {
      "main": [
        [
          {
            "node": "Upsert to Supabase",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}