{
  "name": "AusJobTracker - Daily Job Scraper",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 24,
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "schedule-trigger",
      "name": "Daily 8AM Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "actorId": "bebity/linkedin-jobs-scraper",
        "runInput": "={{ JSON.stringify({\n  \"searchQueries\": [\n    \"computational biology Australia\",\n    \"bioinformatics Australia\",\n    \"genomics Australia\",\n    \"bioinformatician Australia\"\n  ],\n  \"location\": \"Australia\",\n  \"maxItems\": 50,\n  \"scrapeJobDetails\": true\n}) }}",
        "options": {
          "waitForFinish": true,
          "timeout": 300
        }
      },
      "id": "apify-linkedin",
      "name": "Apify LinkedIn Jobs",
      "type": "n8n-nodes-base.apify",
      "typeVersion": 1,
      "position": [
        220,
        0
      ],
      "credentials": {
        "apifyApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Process and score job listings\nconst primaryKeywords = [\n  'computational biology', 'bioinformatics', 'bacterial genomics',\n  'microbial genomics', 'antibiotic resistance', 'microbial evolution',\n  'genomics', 'sequencing data analysis', 'computational biologist',\n  'bioinformatician', 'genome analysis', 'bacterial evolution',\n  'crop genomics', 'plant genomics', 'agricultural genomics'\n];\n\nconst secondaryKeywords = [\n  'plant biology', 'environmental microbiology', 'pharmacology',\n  'systems biology', 'computational scientist', 'data scientist',\n  'research scientist', 'postdoctoral', 'fellowship', 'microbiology',\n  'metagenomics', 'crop science'\n];\n\nconst australianLocations = [\n  'australia', 'melbourne', 'sydney', 'brisbane', 'adelaide',\n  'perth', 'canberra', 'hobart', 'darwin', 'nsw', 'vic', 'qld',\n  'sa', 'wa', 'act', 'nt', 'tas'\n];\n\nfunction scoreJob(job) {\n  let score = 0;\n  const title = (job.title || '').toLowerCase();\n  const description = (job.description || '').toLowerCase();\n  const location = (job.location || '').toLowerCase();\n  const matchedKeywords = [];\n\n  // Check Australian location\n  const isAustralian = australianLocations.some(loc => location.includes(loc));\n  if (!isAustralian) return null;\n\n  // Score primary keywords\n  for (const keyword of primaryKeywords) {\n    if (title.includes(keyword)) {\n      score += 15;\n      matchedKeywords.push(keyword);\n    } else if (description.includes(keyword)) {\n      score += 10;\n      matchedKeywords.push(keyword);\n    }\n  }\n\n  // Score secondary keywords\n  for (const keyword of secondaryKeywords) {\n    if (title.includes(keyword)) {\n      score += 7;\n      matchedKeywords.push(keyword);\n    } else if (description.includes(keyword)) {\n      score += 5;\n      matchedKeywords.push(keyword);\n    }\n  }\n\n  // Minimum score threshold\n  if (score < 10) return null;\n\n  return {\n    title: job.title,\n    organization: job.company || job.companyName || 'Unknown',\n    location: job.location,\n    description: job.description || '',\n    brief_description: (job.description || '').substring(0, 500),\n    url: job.link || job.url || job.jobUrl,\n    source_website: 'linkedin.com',\n    date_posted: job.postedAt || job.datePosted || null,\n    job_type: job.employmentType || job.jobType || null,\n    sector: 'private',\n    relevance_score: score,\n    keywords_matched: JSON.stringify([...new Set(matchedKeywords)]),\n    raw_data: JSON.stringify(job),\n    is_active: true\n  };\n}\n\n// Process all items\nconst results = [];\nfor (const item of $input.all()) {\n  const job = item.json;\n  const scored = scoreJob(job);\n  if (scored) {\n    results.push({ json: scored });\n  }\n}\n\nreturn results;"
      },
      "id": "process-jobs",
      "name": "Score & Filter Jobs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        440,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT id, url FROM jobs WHERE url = '{{ $json.url }}' LIMIT 1",
        "options": {}
      },
      "id": "check-duplicate",
      "name": "Check Duplicate",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        660,
        0
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "is-new-job",
              "leftValue": "={{ $json.length }}",
              "rightValue": "0",
              "operator": {
                "type": "number",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-new-job",
      "name": "Is New Job?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "INSERT INTO jobs (title, organization, location, description, brief_description, url, source_website, date_posted, job_type, sector, relevance_score, keywords_matched, raw_data, is_active, date_discovered)\nVALUES (\n  '{{ $('Score & Filter Jobs').item.json.title.replace(/'/g, \"''\") }}',\n  '{{ $('Score & Filter Jobs').item.json.organization.replace(/'/g, \"''\") }}',\n  '{{ $('Score & Filter Jobs').item.json.location.replace(/'/g, \"''\") }}',\n  '{{ $('Score & Filter Jobs').item.json.description.replace(/'/g, \"''\").substring(0, 10000) }}',\n  '{{ $('Score & Filter Jobs').item.json.brief_description.replace(/'/g, \"''\") }}',\n  '{{ $('Score & Filter Jobs').item.json.url }}',\n  '{{ $('Score & Filter Jobs').item.json.source_website }}',\n  {{ $('Score & Filter Jobs').item.json.date_posted ? \"'\" + $('Score & Filter Jobs').item.json.date_posted + \"'\" : 'NULL' }},\n  {{ $('Score & Filter Jobs').item.json.job_type ? \"'\" + $('Score & Filter Jobs').item.json.job_type + \"'\" : 'NULL' }},\n  '{{ $('Score & Filter Jobs').item.json.sector }}',\n  {{ $('Score & Filter Jobs').item.json.relevance_score }},\n  '{{ $('Score & Filter Jobs').item.json.keywords_matched }}',\n  '{{ $('Score & Filter Jobs').item.json.raw_data.replace(/'/g, \"''\").substring(0, 50000) }}',\n  true,\n  NOW()\n)\nRETURNING id, title, organization, location",
        "options": {}
      },
      "id": "insert-job",
      "name": "Insert New Job",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        1100,
        -100
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Aggregate all new jobs for email\nconst newJobs = $input.all().map(item => item.json);\n\nif (newJobs.length === 0) {\n  return [{ json: { skipEmail: true, count: 0 } }];\n}\n\n// Build email body\nconst jobList = newJobs.map(job => \n  `<div style=\"margin-bottom: 20px; padding: 15px; border: 1px solid #ddd; border-radius: 8px;\">\n    <h3 style=\"margin: 0 0 10px 0; color: #2563eb;\">${job.title}</h3>\n    <p style=\"margin: 5px 0; color: #666;\"><strong>${job.organization}</strong> - ${job.location}</p>\n    <p style=\"margin: 10px 0; font-size: 14px;\">${(job.brief_description || '').substring(0, 200)}...</p>\n    <a href=\"${job.url}\" style=\"color: #2563eb; text-decoration: none;\">View Job \u2192</a>\n  </div>`\n).join('');\n\nconst emailBody = `\n<html>\n<body style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 0 auto;\">\n  <h1 style=\"color: #1e40af;\">\ud83e\uddec New Computational Biology Jobs</h1>\n  <p style=\"color: #666;\">Found ${newJobs.length} new job(s) matching your criteria:</p>\n  ${jobList}\n  <hr style=\"border: none; border-top: 1px solid #ddd; margin: 30px 0;\">\n  <p style=\"color: #999; font-size: 12px;\">View all jobs at <a href=\"https://ausjobtracker.vercel.app\">AusJobTracker</a></p>\n</body>\n</html>\n`;\n\nreturn [{\n  json: {\n    skipEmail: false,\n    count: newJobs.length,\n    subject: `\ud83e\uddec ${newJobs.length} New Computational Biology Job${newJobs.length > 1 ? 's' : ''} Found`,\n    body: emailBody\n  }\n}];"
      },
      "id": "aggregate-jobs",
      "name": "Aggregate for Email",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        -100
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "should-send-email",
              "leftValue": "={{ $json.skipEmail }}",
              "rightValue": "false",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if-send-email",
      "name": "Should Send Email?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1540,
        -100
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://n8n.kaulfamily.org/webhook/send-email",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={\n  \"to\": \"YOUR_EMAIL@example.com\",\n  \"subject\": \"{{ $json.subject }}\",\n  \"body\": {{ JSON.stringify($json.body) }}\n}",
        "options": {}
      },
      "id": "send-email",
      "name": "Send Email Notification",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1760,
        -200
      ]
    },
    {
      "parameters": {},
      "id": "no-op-skip",
      "name": "Skip (No New Jobs)",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1100,
        100
      ]
    }
  ],
  "connections": {
    "Daily 8AM Trigger": {
      "main": [
        [
          {
            "node": "Apify LinkedIn Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apify LinkedIn Jobs": {
      "main": [
        [
          {
            "node": "Score & Filter Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score & Filter Jobs": {
      "main": [
        [
          {
            "node": "Check Duplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Duplicate": {
      "main": [
        [
          {
            "node": "Is New Job?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Is New Job?": {
      "main": [
        [
          {
            "node": "Insert New Job",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Skip (No New Jobs)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert New Job": {
      "main": [
        [
          {
            "node": "Aggregate for Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate for Email": {
      "main": [
        [
          {
            "node": "Should Send Email?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Should Send Email?": {
      "main": [
        [
          {
            "node": "Send Email Notification",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "callerPolicy": "workflowsFromSameOwner",
    "errorWorkflow": ""
  },
  "staticData": null,
  "tags": [
    {
      "name": "job-scraping"
    },
    {
      "name": "ausjobtracker"
    }
  ],
  "triggerCount": 1,
  "versionId": "1"
}