{
  "id": "RWmeaIdV3KulXhPd",
  "name": "Scrape job posts from RSS feeds and get instant Telegram alerts with deduplication",
  "tags": [],
  "nodes": [
    {
      "id": "cc5c0284-423f-4a98-a717-0cff4d5d8f79",
      "name": "Sticky Note - Main Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "width": 720,
        "height": 940,
        "content": "## Scrape job posts from RSS feeds and get instant Telegram alerts\n\nAutomatically monitor job boards and RSS feeds on a schedule, filter by your criteria, deduplicate results, and get instant Telegram alerts when new matching jobs are found.\n\n### How it works\n1. **Schedule Trigger** runs every 6 hours (configurable) to check for new job posts.\n2. **HTTP Request** fetches job data from an RSS feed or job board API.\n3. **Parse & Extract** normalizes raw data into clean job objects with title, company, location, URL, salary, tags, and posting date.\n4. **Keyword Filter** matches jobs against your target roles, locations, and exclude terms \u2014 only relevant jobs pass through.\n5. **Deduplication** checks each job URL against a Google Sheet of previously seen jobs. Only truly new jobs continue.\n6. **Log to Sheet** saves every new job to Google Sheets for tracking and history.\n7. **Telegram Alert** sends a formatted message with job details and a direct apply link.\n\n### Setup steps\n1. **Schedule** \u2014 Adjust the interval in the Schedule Trigger (default: every 6 hours).\n2. **Job Source URL** \u2014 Replace the URL in the HTTP Request node with your target RSS feed or API. Examples:\n   - RemoteOK: `https://remoteok.com/api`\n   - Arbeitnow: `https://www.arbeitnow.com/api/job-board-api`\n   - Any RSS feed from LinkedIn, Indeed, etc.\n3. **Keywords** \u2014 Edit `targetRoles`, `targetLocations`, and `excludeTerms` arrays in the Keyword Filter node.\n4. **Google Sheets** \u2014 Connect your credential and set the spreadsheet ID in both Sheet nodes. Create columns: Title, Company name, Location, Url, Description, Posted date, Salary, Matched Role, Scraped date.\n5. **Telegram** \u2014 Create a bot via @BotFather, get your Chat ID, and connect the Telegram credential.\n6. **Test** \u2014 Run manually once to verify jobs flow through correctly.\n\n### Customization\n- Add multiple HTTP Request nodes for different job boards and merge results.\n- Change cron to every 1 hour for high-priority searches.\n- Add salary range filtering in the keyword filter.\n- Replace Telegram with Slack, Discord, WhatsApp, or email.\n- Add an AI node to score/rank jobs by relevance before alerting."
      },
      "typeVersion": 1
    },
    {
      "id": "a5815a2d-045f-4106-813d-d08f04d6febb",
      "name": "Sticky Note - Data Source",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        928,
        912
      ],
      "parameters": {
        "color": 6,
        "width": 520,
        "height": 240,
        "content": "## \ud83d\udce5 Data Source\nSchedule trigger + HTTP request to fetch job posts from RSS/API"
      },
      "typeVersion": 1
    },
    {
      "id": "a8dcbb02-4ccb-4669-8b9e-d8352a2636ff",
      "name": "Sticky Note - Parse Filter",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1472,
        912
      ],
      "parameters": {
        "color": 6,
        "width": 540,
        "height": 240,
        "content": "## \ud83d\udd0d Parse & Filter\nExtract job fields, then filter by keywords, location, and role"
      },
      "typeVersion": 1
    },
    {
      "id": "badff48d-3b8b-44fe-8d77-4637020a7c33",
      "name": "Sticky Note - Deduplicate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2176,
        832
      ],
      "parameters": {
        "color": 6,
        "width": 464,
        "height": 240,
        "content": "## \ud83e\uddf9 Deduplicate & Log\nCheck against previously seen jobs, save new ones to Google Sheets"
      },
      "typeVersion": 1
    },
    {
      "id": "25c2ccb0-d793-4486-8e9f-e770362be22e",
      "name": "Sticky Note - Alerts",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2864,
        784
      ],
      "parameters": {
        "color": 6,
        "width": 540,
        "height": 240,
        "content": "## \ud83d\udce3 Alerts\nSend formatted job notifications to Telegram with apply links"
      },
      "typeVersion": 1
    },
    {
      "id": "27446981-2b9f-4c8f-a45a-8fd035d3c4d7",
      "name": "Sticky Note - Warning URL",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1504,
        1200
      ],
      "parameters": {
        "color": 3,
        "width": 280,
        "height": 156,
        "content": "## \u26a0\ufe0f Replace with your job board URL\nUpdate the HTTP Request URL to your target RSS feed or API. See the main overview sticky for example URLs."
      },
      "typeVersion": 1
    },
    {
      "id": "2cf9c4aa-e123-4a01-ba20-4c02aae83bf6",
      "name": "Sticky Note - Warning Keywords",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1824,
        1200
      ],
      "parameters": {
        "color": 3,
        "width": 280,
        "height": 140,
        "content": "## \u26a0\ufe0f Edit your keywords\nUpdate `targetRoles`, `targetLocations`, and `excludeTerms` arrays to match your job search criteria."
      },
      "typeVersion": 1
    },
    {
      "id": "203cb7b5-3baf-48f1-a236-6587b1afd850",
      "name": "\u23f0 Every 6 Hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        752,
        1024
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 6
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "ced7e0fd-640c-4466-974f-cf18b5b734b6",
      "name": "\ud83c\udf10 Fetch Job Posts",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1152,
        1008
      ],
      "parameters": {
        "url": "https://remoteok.com/api",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "8d5a015a-facd-4209-bdc4-1014bbd9d34a",
      "name": "\ud83d\udd27 Parse & Extract Jobs",
      "type": "n8n-nodes-base.code",
      "position": [
        1568,
        992
      ],
      "parameters": {
        "jsCode": "// Parse and normalize job data from various sources\n// Adjust field mappings based on your job board's API response\n\nconst items = $input.all();\nconst jobs = [];\n\nfor (const item of items) {\n  const data = item.json;\n  \n  // Skip non-job entries (e.g., RemoteOK's first item is metadata)\n  if (!data.position && !data.title && !data.job_title) continue;\n  \n  const job = {\n    title: data.position || data.title || data.job_title || 'Unknown Title',\n    company: data.company || data.company_name || data.organization || 'Unknown Company',\n    location: data.location || data.candidate_required_location || data.job_location || 'Remote',\n    url: data.url || data.apply_url || data.link || '',\n    description: (data.description || data.summary || data.job_description || '').substring(0, 500),\n    tags: data.tags || data.categories || data.keywords || [],\n    salary: data.salary || data.salary_range || data.compensation || 'Not specified',\n    postedDate: data.date || data.publication_date || data.created_at || new Date().toISOString(),\n    source: 'RemoteOK',\n    scrapedAt: new Date().toISOString(),\n    // Create a unique ID from URL or title+company combo\n    uniqueId: (data.url || data.apply_url || data.link || `${data.position}-${data.company}`).toLowerCase().trim()\n  };\n  \n  // Only add if we have minimum required fields\n  if (job.title !== 'Unknown Title' && job.url) {\n    jobs.push({ json: job });\n  }\n}\n\n// If no jobs parsed, return empty with a flag\nif (jobs.length === 0) {\n  return [{ json: { noJobs: true, message: 'No jobs found in this batch' } }];\n}\n\nreturn jobs;"
      },
      "typeVersion": 2
    },
    {
      "id": "95a79ff1-580b-4ee3-9956-d263f03653ae",
      "name": "\ud83c\udfaf Keyword Filter",
      "type": "n8n-nodes-base.code",
      "position": [
        1792,
        992
      ],
      "parameters": {
        "jsCode": "// ============================================\n// KEYWORD FILTER - Customize these arrays!\n// ============================================\n\nconst targetRoles = [\n  // Add your target job titles/keywords here\n  \"automation engineer\", \"workflow automation\",\n  \"data engineer\", \"integration engineer\",\n  \"marketing automation\", \"sales automation\",\n  \"ai engineer\", \"machine learning\",\n  \"devops\", \"backend engineer\"\n];\n\nconst targetLocations = [\n  \"remote\", \"fully remote\", \"work from anywhere\", \"work from home\",\n  \"wfh\", \"worldwide\", \"global\", \"100% remote\", \"distributed\"\n];\n\nconst excludeTerms = [\n  \"director\", \"vp \", \"vice president\", \"head of\", \"chief\", \"c-level\",\n  \"senior manager\", \"principal\", \"architect\"\n];\n\n\n\n\n// ============================================\n// FILTER LOGIC (no changes needed below)\n// ============================================\n\nconst items = $input.all();\nconst matchedJobs = [];\n\nfor (const item of items) {\n  const job = item.json;\n  \n  // Skip if it's the \"no jobs\" flag\n  if (job.noJobs) continue;\n  \n  const titleLower = (job.title || '').toLowerCase();\n  const locationLower = (job.location || '').toLowerCase();\n  const descLower = (job.description || '').toLowerCase();\n  const tagsLower = Array.isArray(job.tags) \n    ? job.tags.map(t => (typeof t === 'string' ? t : t.name || t.label || String(t)).toLowerCase()).join(' ')\n    : '';\n  \n  const searchText = `${titleLower} ${descLower} ${tagsLower}`;\n  \n  // Check role match (title or tags)\n  const roleMatch = targetRoles.some(role => \n    titleLower.includes(role) || tagsLower.includes(role)\n  );\n  \n  // Check location match\n  const locationMatch = targetLocations.some(loc => \n    locationLower.includes(loc)\n  );\n  \n  // Check exclusions\n  const isExcluded = excludeTerms.some(term => \n    titleLower.includes(term)\n  );\n  \n  // Must match role AND location, and NOT be excluded\n  if (roleMatch && locationMatch && !isExcluded) {\n    matchedJobs.push({\n      json: {\n        ...job,\n        matchedRole: targetRoles.find(r => titleLower.includes(r) || tagsLower.includes(r)),\n        matchedLocation: targetLocations.find(l => locationLower.includes(l)),\n        filterPassedAt: new Date().toISOString()\n      }\n    });\n  }\n}\n\nif (matchedJobs.length === 0) {\n  return [{ json: { noMatches: true, message: `No jobs matched filters. Checked ${items.length} jobs.`, checkedAt: new Date().toISOString() } }];\n}\n\nreturn matchedJobs;"
      },
      "typeVersion": 2
    },
    {
      "id": "44558676-f7f6-451c-9cbf-4e9605dd48a5",
      "name": "\u2705 Has Valid Jobs?",
      "type": "n8n-nodes-base.if",
      "position": [
        2016,
        992
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "has-url",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.url }}",
              "rightValue": ""
            },
            {
              "id": "not-no-matches",
              "operator": {
                "type": "boolean",
                "operation": "notTrue"
              },
              "leftValue": "={{ $json.noMatches }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "9c139a23-f8ab-4233-b258-36bd1390558f",
      "name": "\ud83d\udccb Read Seen Jobs",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2240,
        912
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.title }}",
              "lookupColumn": "title"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1313119612,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit#gid=1313119612",
          "cachedResultName": "jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit",
          "cachedResultName": "Job Tracker"
        }
      },
      "typeVersion": 4.6,
      "alwaysOutputData": true
    },
    {
      "id": "0a30c12a-902e-4044-b598-456ffd4b1340",
      "name": "\ud83e\uddf9 Deduplicate",
      "type": "n8n-nodes-base.code",
      "position": [
        2464,
        912
      ],
      "parameters": {
        "jsCode": "// Deduplicate: compare current jobs against previously seen URLs\n\nconst currentJobs = $('\u2705 Has Valid Jobs?').all();\nconst seenJobsRaw = $('\ud83d\udccb Read Seen Jobs').all();\n\n// Build a Set of previously seen URLs for O(1) lookup\nconst seenUrls = new Set();\nfor (const item of seenJobsRaw) {\n  const url = (item.json.URL || item.json.url || item.json.uniqueId || '').toLowerCase().trim();\n  if (url) seenUrls.add(url);\n}\n\nconst newJobs = [];\n\nfor (const item of currentJobs) {\n  const job = item.json;\n  const jobUrl = (job.url || job.uniqueId || '').toLowerCase().trim();\n  \n  if (jobUrl && !seenUrls.has(jobUrl)) {\n    newJobs.push({\n      json: {\n        ...job,\n        isNew: true,\n        deduplicatedAt: new Date().toISOString()\n      }\n    });\n  }\n}\n\nif (newJobs.length === 0) {\n  return [{ json: { noNewJobs: true, message: `All ${currentJobs.length} matched jobs were already seen.`, checkedAt: new Date().toISOString() } }];\n}\n\nreturn newJobs;"
      },
      "typeVersion": 2
    },
    {
      "id": "cb5c3ec1-0760-464c-befa-b8145e491d9f",
      "name": "\ud83c\udd95 New Jobs Only?",
      "type": "n8n-nodes-base.if",
      "position": [
        2688,
        912
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "is-new-job",
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.isNew }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "6fa826da-9cb3-42cb-8d1f-021a4c0920e9",
      "name": "\ud83d\udcca Log New Jobs",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2912,
        864
      ],
      "parameters": {
        "columns": {
          "value": {
            "Salary": "={{ $json.salary }}",
            "Location": "={{ $json.location }}",
            "Matched Role": "={{ $json.matchedRole }}"
          },
          "schema": [
            {
              "id": "Title ",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Title ",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Company name",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Company name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Location",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Location",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Url",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Description",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Description",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Posted date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Posted date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Salary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Salary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Matched Role",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Matched Role",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Scraped date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Scraped date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1313119612,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit#gid=1313119612",
          "cachedResultName": "jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit",
          "cachedResultName": "Job Tracker"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "499d208e-c88a-422e-af12-b3d78f9b6429",
      "name": "\ud83d\udcf2 Telegram Alert",
      "type": "n8n-nodes-base.telegram",
      "position": [
        3136,
        864
      ],
      "parameters": {
        "text": "=\ud83d\ude80 *New Job Alert!*\n\n\ud83d\udcbc *{{ $json.title }}*\n\ud83c\udfe2 {{ $json.company }}\n\ud83d\udccd {{ $json.location }}\n\ud83d\udcb0 {{ $json.salary }}\n\n\ud83c\udff7\ufe0f _{{ Array.isArray($json.tags) ? $json.tags.slice(0, 5).join(', ') : $json.tags }}_\n\n\ud83d\udd17 [Apply Now]({{ $json.url }})\n\n\ud83d\udcc5 Posted: {{ $json.postedDate }}\n\ud83c\udfaf Matched: {{ $json.matchedRole }} \u00b7 {{ $json.matchedLocation }}",
        "chatId": "YOUR_TELEGRAM_CHAT_ID",
        "additionalFields": {
          "parse_mode": "Markdown",
          "appendAttribution": false,
          "disable_web_page_preview": true
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "0bd964ca-0a8b-41a2-8c3e-50d9bdd880cc",
      "name": "\ud83d\udca4 No New Jobs",
      "type": "n8n-nodes-base.code",
      "position": [
        2912,
        1088
      ],
      "parameters": {
        "jsCode": "// Summary when no new jobs found\nconst message = $json.message || 'No new matching jobs found this cycle.';\nconst timestamp = new Date().toISOString();\n\nreturn {\n  json: {\n    status: 'no_new_jobs',\n    message: message,\n    checkedAt: timestamp,\n    nextCheck: 'In 6 hours'\n  }\n};"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "0859e4ce-1fbd-484a-8d63-2c4d59d965f4",
  "connections": {
    "\ud83e\uddf9 Deduplicate": {
      "main": [
        [
          {
            "node": "\ud83c\udd95 New Jobs Only?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u23f0 Every 6 Hours": {
      "main": [
        [
          {
            "node": "\ud83c\udf10 Fetch Job Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcca Log New Jobs": {
      "main": [
        [
          {
            "node": "\ud83d\udcf2 Telegram Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2705 Has Valid Jobs?": {
      "main": [
        [
          {
            "node": "\ud83d\udccb Read Seen Jobs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udca4 No New Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83c\udd95 New Jobs Only?": {
      "main": [
        [
          {
            "node": "\ud83d\udcca Log New Jobs",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "\ud83d\udca4 No New Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83c\udfaf Keyword Filter": {
      "main": [
        [
          {
            "node": "\u2705 Has Valid Jobs?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udccb Read Seen Jobs": {
      "main": [
        [
          {
            "node": "\ud83e\uddf9 Deduplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83c\udf10 Fetch Job Posts": {
      "main": [
        [
          {
            "node": "\ud83d\udd27 Parse & Extract Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd27 Parse & Extract Jobs": {
      "main": [
        [
          {
            "node": "\ud83c\udfaf Keyword Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}