{
  "name": "Career Ops - Agent 1 - Job Discovery (Multi-Category)",
  "nodes": [
    {
      "parameters": {},
      "id": "trigger-node",
      "name": "Manual Trigger (swap for Schedule Trigger once tested)",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const searchTerms = [\n  'help desk support',\n  'desktop support technician',\n  'IT support specialist',\n  'network support technician',\n  'cybersecurity analyst',\n  'SOC analyst',\n  'security compliance analyst',\n  'IT operations manager',\n  'operations manager',\n  'assistant general manager',\n  'branch manager',\n  'restaurant manager',\n  'customer service manager',\n  'automation specialist',\n  'AI engineer',\n  'warehouse associate',\n  'general laborer',\n  'construction laborer',\n  'field technician'\n];\n\nreturn searchTerms.map(term => ({ json: { what: term } }));"
      },
      "id": "categories-node",
      "name": "Job Search Categories",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        300
      ]
    },
    {
      "parameters": {
        "url": "https://api.adzuna.com/v1/api/jobs/us/search/1",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "app_id",
              "value": "YOUR_ADZUNA_APP_ID"
            },
            {
              "name": "app_key",
              "value": "YOUR_ADZUNA_APP_KEY"
            },
            {
              "name": "what",
              "value": "={{$json.what}}"
            },
            {
              "name": "where",
              "value": "Hartford, CT"
            },
            {
              "name": "distance",
              "value": "40"
            },
            {
              "name": "results_per_page",
              "value": "20"
            },
            {
              "name": "content-type",
              "value": "application/json"
            }
          ]
        },
        "options": {
          "batching": {
            "batch": {
              "batchSize": 1,
              "batchInterval": 2000
            }
          }
        }
      },
      "id": "adzuna-search-node",
      "name": "Adzuna Search",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        660,
        300
      ],
      "notes": "SECURITY: move app_id/app_key into an n8n Credential before this runs unattended."
    },
    {
      "parameters": {
        "fieldToSplitOut": "results",
        "options": {}
      },
      "id": "split-node",
      "name": "Split Results",
      "type": "n8n-nodes-base.splitOut",
      "typeVersion": 1,
      "position": [
        880,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const HOME_LAT = 41.9959;\nconst HOME_LNG = -72.3020;\nconst MAX_MILES = 45;\n\nfunction haversineMiles(lat1, lon1, lat2, lon2) {\n  const R = 3958.8;\n  const dLat = (lat2 - lat1) * Math.PI / 180;\n  const dLon = (lon2 - lon1) * Math.PI / 180;\n  const a = Math.sin(dLat / 2) ** 2 +\n    Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *\n    Math.sin(dLon / 2) ** 2;\n  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n}\n\nconst KEYWORDS = [\n  'active directory', 'azure', 'intune', 'security+', 'servicenow', 'help desk',\n  'desktop support', 'network', 'windows server', 'microsoft 365', 'troubleshooting',\n  'vpn', 'endpoint', 'system administration',\n  'cybersecurity', 'soc analyst', 'incident response', 'vulnerability', 'threat',\n  'compliance', 'risk management',\n  'manager', 'management', 'supervisor', 'team lead', 'operations', 'branch',\n  'general manager', 'leadership', 'staff',\n  'automation', 'ai', 'artificial intelligence', 'workflow', 'process improvement',\n  'customer service', 'customer support', 'crm', 'client relations'\n];\n\nconst LOW_BARRIER_TITLE_MARKERS = [\n  'warehouse', 'laborer', 'labor', 'construction', 'general labor',\n  'driver', 'landscaping', 'installer', 'maintenance worker', 'stock'\n];\n\nfunction isLowBarrier(title) {\n  const t = title.toLowerCase();\n  return LOW_BARRIER_TITLE_MARKERS.some(m => t.includes(m));\n}\n\nfunction classifyCategory(title) {\n  const t = title.toLowerCase();\n  if (t.includes('cyber') || t.includes('soc analyst') || t.includes('security compliance') || t.includes('security analyst')) return 'Cybersecurity';\n  if (t.includes('construction')) return 'Construction';\n  if (t.includes('warehouse') || t.includes('driver') || t.includes('landscaping') || t.includes('installer') || t.includes('maintenance worker') || t.includes('stock') || t.includes('general labor')) return 'General Labor';\n  if (t.includes('manager') || t.includes('supervisor') || t.includes('branch') || t.includes('general manager')) return 'Management';\n  if (t.includes(' ai ') || t.startsWith('ai ') || t.includes('artificial intelligence') || t.includes('automation')) return 'AI/Automation';\n  if (t.includes('customer service') || t.includes('customer support') || t.includes('client relations')) return 'Customer Service';\n  if (t.includes('help desk') || t.includes('desktop support') || t.includes('it support') || t.includes('network') || t.includes('technician') || t.includes('field technician')) return 'IT';\n  return 'Other';\n}\n\nfunction scoreFit(title, description) {\n  const text = (title + ' ' + description).toLowerCase();\n  let hits = 0;\n  for (const kw of KEYWORDS) {\n    if (text.includes(kw)) hits++;\n  }\n  return Math.min(5, Math.max(1, Math.ceil(hits / 2)));\n}\n\nfunction formatSalary(min, max) {\n  if (!min && !max) return '';\n  const fmt = (n) => '$' + Math.round(n).toLocaleString('en-US');\n  if (min && max) return fmt(min) + ' - ' + fmt(max);\n  return fmt(min || max);\n}\n\nconst items = $input.all();\nconst seen = new Set();\nconst output = [];\n\nfor (const item of items) {\n  const job = item.json;\n  if (!job.id || seen.has(job.id)) continue;\n  seen.add(job.id);\n\n  if (!job.latitude || !job.longitude) continue;\n  const distance = haversineMiles(HOME_LAT, HOME_LNG, parseFloat(job.latitude), parseFloat(job.longitude));\n  if (distance > MAX_MILES) continue;\n\n  const title = job.title || '';\n  const lowBarrier = isLowBarrier(title);\n  const keywordScore = scoreFit(title, job.description || '');\n  const fitScore = lowBarrier ? Math.max(3, keywordScore) : keywordScore;\n  const applyUrl = job.redirect_url || '';\n\n  output.push({\n    json: {\n      adzuna_id: String(job.id),\n      title: title,\n      company: job.company ? job.company.display_name : '',\n      location: job.location ? job.location.display_name : '',\n      category: classifyCategory(title),\n      job_type: job.contract_time === 'full_time' ? 'Full-time' : (job.contract_time === 'part_time' ? 'Part-time' : ''),\n      salary_range: formatSalary(job.salary_min, job.salary_max),\n      distance_miles: Math.round(distance),\n      description: job.description || '',\n      apply_url: applyUrl,\n      apply_link_formula: applyUrl ? ('=HYPERLINK(\"' + applyUrl + '\",\"Apply \\u2192\")') : '',\n      created: job.created,\n      low_barrier_role: lowBarrier,\n      fit_score: fitScore\n    }\n  });\n}\n\nreturn output;"
      },
      "id": "filter-score-node",
      "name": "Dedupe + Filter Distance + Score Fit",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose"
          },
          "conditions": [
            {
              "leftValue": "={{$json.fit_score}}",
              "rightValue": 3,
              "operator": {
                "type": "number",
                "operation": "gte"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "filter-fit-node",
      "name": "Keep Fit Score 3+",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2,
      "position": [
        1320,
        300
      ]
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "1JdchogQzAHxzXEGD2STDYoz2O4V_qB7nSDytFAbrTcg",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Applications",
          "mode": "name"
        },
        "operation": "appendOrUpdate",
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Adzuna ID"
          ],
          "value": {
            "Date Found": "={{$now.format('yyyy-MM-dd')}}",
            "Job Title": "={{$json.title}}",
            "Company": "={{$json.company}}",
            "Location": "={{$json.location}}",
            "Category": "={{$json.category}}",
            "Job Type": "={{$json.job_type}}",
            "Salary Range": "={{$json.salary_range}}",
            "Fit Score": "={{$json.fit_score}}",
            "Source": "Adzuna",
            "Apply Link": "={{$json.apply_link_formula}}",
            "Job Description": "={{$json.description}}",
            "Status": "Ready to Apply",
            "Adzuna ID": "={{$json.adzuna_id}}"
          }
        },
        "options": {
          "valueInputMode": "USER_ENTERED"
        }
      },
      "id": "append-sheet-node",
      "name": "Append or Update Tracker Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1540,
        300
      ],
      "notes": "Matches on Adzuna ID - re-running this daily is now safe from creating duplicate rows. LIMITATION: if the same job still appears in Adzuna's results on a later run, this WILL overwrite that row's Status back to 'Ready to Apply', even if you'd already marked it Applied. This only happens if a specific listing stays live in Adzuna's index across multiple runs. Acceptable for now - if it becomes a real problem, the fix is a lookup-and-skip step before the write, which we can add later."
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 7,
              "triggerAtMinute": 0
            }
          ]
        }
      },
      "id": "schedule-trigger-node",
      "name": "Daily Schedule (7:00 AM)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        220,
        460
      ]
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst count = items.length;\n\nif (count === 0) {\n  return [{ json: { send: false, message: '' } }];\n}\n\n// Sort by fit score, highest first, show top 10 in the message\nconst sorted = [...items].sort((a, b) => (b.json.fit_score || 0) - (a.json.fit_score || 0));\nconst top = sorted.slice(0, 10);\n\nlet msg = `\\ud83d\\udd0d Career Ops Daily Digest\\n${count} new job match${count === 1 ? '' : 'es'} added today\\n\\n`;\n\ntop.forEach((item, i) => {\n  const j = item.json;\n  msg += `${i + 1}. ${j.title}\\n`;\n  msg += `   ${j.company} \\u2014 ${j.location}\\n`;\n  msg += `   Fit: ${j.fit_score}/5${j.salary_range ? ' \\u2014 ' + j.salary_range : ''}\\n`;\n  msg += `   Apply: ${j.apply_url}\\n\\n`;\n});\n\nif (count > 10) {\n  msg += `+ ${count - 10} more \\u2014 see the full list in your tracker.\\n\\n`;\n}\n\nmsg += `Full tracker: https://docs.google.com/spreadsheets/d/1JdchogQzAHxzXEGD2STDYoz2O4V_qB7nSDytFAbrTcg/edit`;\n\nreturn [{ json: { send: true, message: msg } }];"
      },
      "id": "digest-format-node",
      "name": "Format Telegram Digest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        300
      ]
    },
    {
      "parameters": {
        "chatId": "7864873212",
        "text": "={{ $json.message }}",
        "additionalFields": {}
      },
      "id": "telegram-send-node",
      "name": "Send Telegram Digest",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        1980,
        300
      ],
      "notes": "Set credential to your existing Telegram bot (Bode_First_OpenClaw_bot / @teslims_bot) - same one your OpenClaw agents already use. If not already saved in this n8n instance, create it with the bot token from BotFather."
    }
  ],
  "connections": {
    "Manual Trigger (swap for Schedule Trigger once tested)": {
      "main": [
        [
          {
            "node": "Job Search Categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Job Search Categories": {
      "main": [
        [
          {
            "node": "Adzuna Search",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Adzuna Search": {
      "main": [
        [
          {
            "node": "Split Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Results": {
      "main": [
        [
          {
            "node": "Dedupe + Filter Distance + Score Fit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Dedupe + Filter Distance + Score Fit": {
      "main": [
        [
          {
            "node": "Keep Fit Score 3+",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keep Fit Score 3+": {
      "main": [
        [
          {
            "node": "Append to Tracker Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Schedule (7:00 AM)": {
      "main": [
        [
          {
            "node": "Job Search Categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append or Update Tracker Sheet": {
      "main": [
        [
          {
            "node": "Format Telegram Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Telegram Digest": {
      "main": [
        [
          {
            "node": "Send Telegram Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}