{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "a13633fc-3429-4840-8a86-0f3f552fc4f5",
      "name": "Check for foreign characters",
      "type": "n8n-nodes-base.filter",
      "position": [
        240,
        -16
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "da8372f6-230d-4bdd-9c59-a6ead041739e",
              "operator": {
                "type": "string",
                "operation": "regex"
              },
              "leftValue": "={{ $json.title }}",
              "rightValue": "=^[\\x00-\\x7F]+$"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b52a5f5f-fd41-4dab-8b5b-a2e1935ae0e0",
      "name": "Title, Budget, Link, Posted, Date, Job Description, Skills, Categories",
      "type": "n8n-nodes-base.code",
      "position": [
        816,
        -16
      ],
      "parameters": {
        "jsCode": "/**\n * Function node (not Function Item)\n * Processes all incoming items and returns one item per job.\n */\nconst inputs = $input.all();\n\nconst safeMultiDecode = (str, times = 3) => {\n  let prev = str, cur = str;\n  for (let i = 0; i < times; i++) {\n    try { cur = decodeURIComponent(prev); } catch { break; }\n    if (cur === prev) break;\n    prev = cur;\n  }\n  return cur;\n};\n\nconst tryExtractParam = (urlStr) => {\n  if (typeof urlStr !== \"string\" || !urlStr) return \"\";\n  try {\n    const u = new URL(urlStr);\n    const param = u.searchParams.get(\"url\");\n    if (param) return safeMultiDecode(param);\n    if (u.hostname.includes(\"upwork.com\")) return urlStr;\n    return \"\";\n  } catch {\n    const part = urlStr.split(\"url=\")[1];\n    return part ? safeMultiDecode(part) : \"\";\n  }\n};\n\nconst formatPosted = (pubDateStr) => {\n  if (typeof pubDateStr !== \"string\" || !pubDateStr.trim()) {\n    return { posted: \"\", date: \"\" };\n  }\n  const pubDate = new Date(pubDateStr);\n  if (isNaN(pubDate.getTime())) return { posted: \"\", date: \"\" };\n\n  const now = new Date();\n  const diffMs = now - pubDate;\n  const diffMinutes = Math.floor(diffMs / 60000);\n  const diffHours = Math.floor(diffMinutes / 60);\n  const diffDays = Math.floor(diffHours / 24);\n\n  let posted = \"\";\n  if (diffMinutes < 1) posted = \"Posted just now\";\n  else if (diffMinutes < 60) posted = `Posted ${diffMinutes} minute${diffMinutes !== 1 ? \"s\" : \"\"} ago`;\n  else if (diffHours < 24) posted = `Posted ${diffHours} hour${diffHours !== 1 ? \"s\" : \"\"} ago`;\n  else posted = `Posted ${diffDays} day${diffDays !== 1 ? \"s\" : \"\"} ago`;\n\n  const formattedDate = pubDate.toLocaleDateString(\"en-GB\", { day: \"numeric\", month: \"long\", year: \"numeric\" });\n  return { posted, date: formattedDate };\n};\n\nconst outputs = inputs.map(({ json }) => {\n  const out = {};\n\n  // 1) Title \u2192 { title, budget }\n  (() => {\n    const input = json?.title;\n    if (typeof input !== \"string\") {\n      out.title = \"\";\n      out.budget = \"\";\n      return;\n    }\n    const regex = /^(.*?)\\s*\\((.*?)\\)$/;\n    const match = input.match(regex);\n    out.title = match ? match[1].trim() : input;\n    out.budget = match ? match[2].trim() : \"\";\n  })();\n\n  // 2) Link \u2192 upwork_link\n  out.upwork_link = tryExtractParam(json?.link) || \"\";\n\n  // 3) pubDate \u2192 { posted, date }\n  Object.assign(out, formatPosted(json?.pubDate));\n\n  // 4) content \u2192 { job_description, skills, categories }\n  (() => {\n    const raw = json?.content;\n    if (typeof raw !== \"string\" || !raw) {\n      out.job_description = \"\";\n      out.skills = \"\";\n      out.categories = \"\";\n      return;\n    }\n    const text = raw\n      .replace(/&amp;/g, \"&\")\n      .replace(/&nbsp;/g, \" \")\n      .replace(/\\r\\n/g, \"\\n\")\n      .replace(/\\r/g, \"\\n\");\n\n    const skillsMatch = text.match(/^\\s*Skills:\\s*([^\\n\\r]+)/im);\n    const categoriesMatch = text.match(/^\\s*Categories:\\s*([^\\n\\r]+)/im);\n\n    out.skills = skillsMatch ? skillsMatch[1].trim() : \"\";\n    out.categories = categoriesMatch ? categoriesMatch[1].trim() : \"\";\n\n    out.job_description = text\n      .replace(/^\\s*Skills:.*$/gim, \"\")\n      .replace(/^\\s*Categories:.*$/gim, \"\");\n  })();\n\n  return { json: out };\n});\n\nreturn outputs;"
      },
      "typeVersion": 2
    },
    {
      "id": "407c809a-89a6-4727-b6af-f7596077bf66",
      "name": "Append row in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1776,
        -32
      ],
      "parameters": {
        "columns": {
          "value": {
            "DATE": "={{ $json.date }}",
            "TITLE": "={{ $json.title }}",
            "BUDGET": "={{ $json.budget }}",
            "POSTED": "={{ $json.posted }}",
            "SKILLS": "={{ $json.skills }}",
            "CATEGORIES": "={{ $json.categories }}",
            "JOB DESCRIPTION": "={{ $json.job_description }}",
            "UPWORK JOB LINK": "={{ $json.upwork_link }}"
          },
          "schema": [
            {
              "id": "TITLE",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "TITLE",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "BUDGET",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "BUDGET",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "POSTED",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "POSTED",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "JOB DESCRIPTION",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "JOB DESCRIPTION",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "DATE",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "DATE",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "SKILLS",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "SKILLS",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "CATEGORIES",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "CATEGORIES",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "UPWORK JOB LINK",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "UPWORK JOB LINK",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_66goIMtz3uZpxzhDGvG9a1GZ6t96i7Q72o6wzZIJ2s/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1_66goIMtz3uZpxzhDGvG9a1GZ6t96i7Q72o6wzZIJ2s",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_66goIMtz3uZpxzhDGvG9a1GZ6t96i7Q72o6wzZIJ2s/edit?usp=drivesdk",
          "cachedResultName": "Upwork Jobs Automation"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "eff67652-6bd6-4ccc-8fb6-0b1a443b40cf",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -304,
        -16
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 3
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "2cb9b9fc-0221-4039-8e32-6fb1d45c6d11",
      "name": "RSS Read",
      "type": "n8n-nodes-base.rssFeedRead",
      "position": [
        -48,
        -16
      ],
      "parameters": {
        "url": "https://www.vollna.com/rss/rftHMpSQCGeEfr2Zwjzb",
        "options": {
          "customFields": ""
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "9e4f6014-dce7-4923-9cd3-b1655f1ac925",
      "name": "Sort",
      "type": "n8n-nodes-base.sort",
      "position": [
        512,
        -16
      ],
      "parameters": {
        "options": {},
        "sortFieldsUi": {
          "sortField": [
            {
              "order": "descending",
              "fieldName": "pubDate"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c6d4316a-f13f-464d-b3d7-7315f50b1a28",
      "name": "Get row(s) in sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1088,
        144
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupValue": "={{ $json.upwork_link }}",
              "lookupColumn": "UPWORK JOB LINK"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_66goIMtz3uZpxzhDGvG9a1GZ6t96i7Q72o6wzZIJ2s/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1_66goIMtz3uZpxzhDGvG9a1GZ6t96i7Q72o6wzZIJ2s",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1_66goIMtz3uZpxzhDGvG9a1GZ6t96i7Q72o6wzZIJ2s/edit?usp=drivesdk",
          "cachedResultName": "Upwork Jobs Automation"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "5925e47c-7174-4811-acbf-5aa7499922bc",
      "name": "Compare Datasets",
      "type": "n8n-nodes-base.compareDatasets",
      "position": [
        1360,
        -16
      ],
      "parameters": {
        "options": {},
        "mergeByFields": {
          "values": [
            {
              "field1": "upwork_link",
              "field2": "UPWORK JOB LINK"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "dd503da0-f241-42e2-a8dc-14794fc73bef",
      "name": "Send a message",
      "type": "n8n-nodes-base.slack",
      "position": [
        2112,
        -32
      ],
      "parameters": {
        "text": "=New Job Alert :fire: :fire:\n\n{{ $json.TITLE }} - {{ $json.BUDGET }}\n\n{{ $json.POSTED }}\n\n{{ $json['UPWORK JOB LINK'] }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C09DV2HQ2M9",
          "cachedResultName": "n8n-jobs"
        },
        "otherOptions": {
          "includeLinkToWorkflow": false
        },
        "authentication": "oAuth2"
      },
      "credentials": {
        "slackOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "7dc37ecc-6fe2-4fa3-8c54-0b1a76221a4d",
      "name": "Send a message1",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2672,
        -32
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "\u2705 A message was just sent to #n8n-jobs!",
        "options": {
          "appendAttribution": false
        },
        "subject": "Slack #n8n",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "86e8692d-42e3-4598-9e59-efecbae62c67",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        2368,
        -32
      ],
      "parameters": {
        "options": {},
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "message"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "27b4377b-c6b7-410c-a896-9e74b5af9c05",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -320,
        -528
      ],
      "parameters": {
        "width": 720,
        "height": 448,
        "content": "Trigger & Input\n\nLocation: Near the Schedule Trigger and RSS Read nodes\nNote Text:\nTrigger & Input\n\n    Runs every 3 minutes\n    Fetches new jobs from Vollna RSS feed\n\nData Cleaning & Filtering\n\nLocation: Next to the Check for foreign characters node\nNote Text:\nFilter Jobs\n\n    Only allow jobs with English (ASCII) titles\n    Prevents non-English/foreign character jobs\n"
      },
      "typeVersion": 1
    },
    {
      "id": "46cf316d-a0d9-493b-b933-f44925065ab2",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        464,
        -528
      ],
      "parameters": {
        "color": 5,
        "width": 800,
        "height": 448,
        "content": "Sorting\n\nLocation: Next to the Sort node\nNote Text:\nSort Jobs\n\n    Sorts jobs by most recent (descending by pubDate)\n\nData Extraction & Formatting\n\nLocation: Next to the Title, Budget, Link, Posted, Date, Job Description, Skills, Categories node\nNote Text:\nExtract & Format Data\n\n    Splits title and budget\n    Extracts Upwork link\n    Formats posted date\n    Extracts job description, skills, and categories\n"
      },
      "typeVersion": 1
    },
    {
      "id": "b7c8fbe2-09b9-4a8f-9d57-372d4ac69e43",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1360,
        -528
      ],
      "parameters": {
        "color": 6,
        "width": 720,
        "height": 448,
        "content": "Duplicate Check\n\nLocation: Between Get row(s) in sheet and Compare Datasets nodes\nNote Text:\nCheck for Duplicates\n\n    Looks up job link in Google Sheet\n    Compares to avoid duplicate entries\n\nAppend to Sheet\n\nLocation: Next to Append row in sheet node\nNote Text:\nSave New Job\n\n    Appends new, unique jobs to Google Sheet\n"
      },
      "typeVersion": 1
    },
    {
      "id": "dca71453-7f0f-4e52-8862-a29e191372fd",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2144,
        -528
      ],
      "parameters": {
        "color": 4,
        "width": 704,
        "height": 448,
        "content": "Notification\n\nLocation: Next to Send a message node\nNote Text:\nSlack Notification\n\n    Sends new job alert to #n8n-jobs Slack channel\n\n(Optional) Email Confirmation\n\nLocation: Next to Aggregate and Send a message1 nodes\nNote Text:\nEmail Confirmation\n\n    Sends email to confirm Slack message was sent\n"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Sort": {
      "main": [
        [
          {
            "node": "Title, Budget, Link, Posted, Date, Job Description, Skills, Categories",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RSS Read": {
      "main": [
        [
          {
            "node": "Check for foreign characters",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Send a message1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send a message": {
      "main": [
        []
      ]
    },
    "Compare Datasets": {
      "main": [
        [
          {
            "node": "Append row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "RSS Read",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append row in sheet": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get row(s) in sheet": {
      "main": [
        [
          {
            "node": "Compare Datasets",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Check for foreign characters": {
      "main": [
        [
          {
            "node": "Sort",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Title, Budget, Link, Posted, Date, Job Description, Skills, Categories": {
      "main": [
        [
          {
            "node": "Get row(s) in sheet",
            "type": "main",
            "index": 0
          },
          {
            "node": "Compare Datasets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}