{
  "id": "Eb8GkcZLfJN6XglR",
  "name": "RemoteOK news fetch",
  "tags": [],
  "nodes": [
    {
      "id": "3de543cc-3d6d-466a-ab78-33a60759c076",
      "name": "Remote ok",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -80,
        0
      ],
      "parameters": {
        "url": "https://remoteok.com/api",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "20582a41-62b0-41ef-aafc-b000cf1f791a",
      "name": "Clean text1",
      "type": "n8n-nodes-base.code",
      "position": [
        480,
        0
      ],
      "parameters": {
        "jsCode": "// In a Function node in n8n\nconst inputData = $input.all();\n\nfunction cleanAllPosts(data) {\n return data.map(item => {\n try {\n // Check if item exists and has the expected structure\n if (!item || typeof item !== 'object') {\n return { cleaned_text: '', error: 'Invalid item structure' };\n }\n\n // Get the text, with multiple fallbacks\n let text = '';\n if (typeof item === 'string') {\n text = item;\n } else if (item.json && item.json.text) {\n text = item.json.text;\n } else if (typeof item.json === 'string') {\n text = item.json;\n } else {\n text = JSON.stringify(item);\n }\n\n // Make sure text is a string\n text = String(text);\n \n // Perform the cleaning operations\n try {\n text = text.replace(/&#x2F;/g, '/');\n text = text.replace(/&#x27;/g, \"'\");\n text = text.replace(/&\\w+;/g, ' ');\n text = text.replace(/<[^>]*>/g, '');\n text = text.replace(/\\|\\s*/g, '| ');\n text = text.replace(/\\s+/g, ' ');\n text = text.replace(/\\s*(https?:\\/\\/[^\\s]+)\\s*/g, '\\n$1\\n');\n text = text.replace(/\\n{3,}/g, '\\n\\n');\n text = text.trim();\n } catch (cleaningError) {\n console.log('Error during text cleaning:', cleaningError);\n // Return original text if cleaning fails\n return { cleaned_text: text, warning: 'Partial cleaning applied' };\n }\n\n return { cleaned_text: text };\n \n } catch (error) {\n console.log('Error processing item:', error);\n return { \n cleaned_text: '', \n error: `Processing error: ${error.message}`,\n original: item\n };\n }\n }).filter(result => result.cleaned_text || result.error); \n}\n\ntry {\n return cleanAllPosts(inputData);\n} catch (error) {\n console.log('Fatal error:', error);\n return [{ \n cleaned_text: '', \n error: `Fatal error: ${error.message}`,\n input: inputData \n }];\n}\n"
      },
      "typeVersion": 2
    },
    {
      "id": "a019ce59-7c8b-47d3-9215-2913e8bbbe94",
      "name": "Code5",
      "type": "n8n-nodes-base.code",
      "position": [
        1140,
        0
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst updatedItems = items.map((item) => {\n  if (item?.json?.data?.port > 5000) {\n    item.json[\"high-port\"] = true;\n  }\n  return item.json;\n});\nreturn updatedItems;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d6412c57-ed82-47b7-a34e-cb78569def21",
      "name": "Telegram1",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1820,
        0
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "chatId": "123456789",
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "8d1d7d16-22cf-4084-87f2-b9a5b08f56ec",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -440,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {}
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "ffeb2eb3-4b98-49c4-8335-b45550ef8606",
      "name": "RemoteOK Jobs",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1400,
        0
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appzlt8d6rIix61J9"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblVWkqYX387ikg7e"
        },
        "columns": {
          "value": {
            "id": "={{ $json.id }}",
            "url": "={{ $json.url }}",
            "logo": "={{ $json.logo }}",
            "tags": "={{ $json.tags }}",
            "source": "={{ $json.source }}",
            "company": "={{ $json.company }}",
            "location": "={{ $json.location }}",
            "position": "={{ $json.position }}",
            "salary_max": "={{ $json.salary_max }}",
            "salary_min": "={{ $json.salary_min }}",
            "description": "={{ $json.description }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true
            },
            {
              "id": "company",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "company",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "position",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "position",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "location",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "location",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "salary_min",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "salary_min",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "salary_max",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "salary_max",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "tags",
              "type": "array",
              "display": true,
              "options": [
                {
                  "name": "developer;design;front-end;digital nomad;accounting;financial;investment;investor;finance;bank;strategy;management;lead;senior;operations;operational;marketing;analytics;legal;sales;digital nomad;health;digital nomad",
                  "value": "developer;design;front-end;digital nomad;accounting;financial;investment;investor;finance;bank;strategy;management;lead;senior;operations;operational;marketing;analytics;legal;sales;digital nomad;health;digital nomad"
                }
              ],
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "tags",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "logo",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "logo",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "description",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "description",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "url",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "source",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "source",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "typecast": true,
          "updateAllMatches": false
        },
        "operation": "upsert"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "86474976-e7d9-4e5e-a256-6a857356ce28",
      "name": "Text-clean",
      "type": "n8n-nodes-base.code",
      "position": [
        700,
        0
      ],
      "parameters": {
        "jsCode": "return items.map(item => {\n  let raw = item.json.cleaned_text;\n\n  try {\n    // Replace common problematic characters\n    raw = raw\n      .replace(/\\n/g, '')            // Remove newlines\n      .replace(/\\r/g, '')            // Remove carriage returns\n      .replace(/\\t/g, ' ')           // Replace tabs with space\n      .replace(/\\\\u0000/g, '')       // Remove null characters\n      .trim();\n\n    // Try parsing the cleaned JSON\n    const parsed = JSON.parse(raw);\n\n    // Return only the 'json' field from parsed result\n    return {\n      json: parsed.json || {}\n    };\n\n  } catch (err) {\n    // If parsing fails, return the original item with error message\n    return {\n      json: {\n        error: 'Parsing failed',\n        reason: err.message,\n        original: raw\n      }\n    };\n  }\n});\n\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2c015df3-8e5c-46ef-ad7d-71b73bffd759",
      "name": "Cleaning the received input",
      "type": "n8n-nodes-base.code",
      "position": [
        220,
        0
      ],
      "parameters": {
        "jsCode": "// Filter out the first item (legal notice / metadata)\nconst jobs = items.filter(item => item.json.id);\n\n// Map and clean each job\nreturn jobs.map(job => ({\n  json: {\n    id: job.json.id,\n    company: job.json.company,\n    position: job.json.position,\n    location: job.json.location,\n    salary_min: job.json.salary_min,\n    salary_max: job.json.salary_max,\n    tags: job.json.tags,\n    logo: job.json.logo,\n    description: job.json.description,\n    url: job.json.url,\n    source: \"Remote OK\"\n  }\n}));\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "ef901f2b-97d5-4871-9784-ce27b87477bb",
      "name": "Salary to string",
      "type": "n8n-nodes-base.code",
      "position": [
        860,
        0
      ],
      "parameters": {
        "jsCode": "return items.map(item => {\n  const min = item.json.salary_min;\n  const max = item.json.salary_max;\n\n  let salaryString = 'Not specified';\n\n  if (min && max) {\n    salaryString = `${min} - ${max}`;\n  } else if (min) {\n    salaryString = `From ${min}`;\n  } else if (max) {\n    salaryString = `Up to ${max}`;\n  }\n  \n  return {\n    json: {\n      ...item.json,\n      salary_string: salaryString\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2e523a11-24e7-4118-83f4-a7d9409aa8a8",
      "name": "Table to a single message",
      "type": "n8n-nodes-base.code",
      "position": [
        1600,
        0
      ],
      "parameters": {
        "jsCode": "function formatJobs(jobs) {\n  if (!Array.isArray(jobs)) return [];\n\n  return jobs.map(job => {\n    const position = job.position || \"No Title\";\n    const company = job.company || \"N/A\";\n    const location = job.location || \"N/A\";\n    const salary = (job.salary_min && job.salary_max) \n      ? `$${job.salary_min} - $${job.salary_max}` \n      : \"N/A\";\n    const description = job.description \n      ? job.description.substring(0, 1000).replace(/\\n/g, ' ') \n      : \"N/A\";\n    const url = job.url || \"N/A\";\n    const source = job.source || \"N/A\";\n\n    const message = `${position}\n\ud83c\udfe2 Company: ${company}\n\ud83c\udf0d Location: ${location}\n\ud83d\udcb0 Salary Range: ${salary}\n\n\ud83d\udcdd Description:\n${description}...\n\n\ud83d\udd17 Apply: ${url}\n\ud83c\udf10 Company Site: ${source}`;\n\n    return { json: { message } };\n  });\n}\n\n// IMPORTANT: Adjust this based on actual input structure\nconst jobs = $input.all().map(item => item.json.fields || {});\n\nreturn formatJobs(jobs);\n"
      },
      "typeVersion": 2
    },
    {
      "id": "284977c4-459e-46ea-a8b4-ef80549d8bbc",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1080,
        -140
      ],
      "parameters": {
        "width": 520,
        "height": 320,
        "content": "## Overview\nPurpose: This workflow fetches remote job listings from RemoteOK, cleans and formats the data, stores it in Airtable, and optionally sends a message via Telegram."
      },
      "typeVersion": 1
    },
    {
      "id": "74e3ac25-3492-460e-bcb1-98981b0f46a1",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -520,
        -240
      ],
      "parameters": {
        "width": 300,
        "height": 440,
        "content": "## Schedule Trigger\n**Purpose**: Triggers the workflow at regular intervals to fetch the latest job listings."
      },
      "typeVersion": 1
    },
    {
      "id": "3c54cd0a-8096-45db-85b3-7aa0880a2744",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -200,
        -240
      ],
      "parameters": {
        "width": 340,
        "height": 440,
        "content": "## Remote ok (HTTP Request)\n**Purpose**: Sends a GET request to https://remoteok.com/api to retrieve job listings.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "8026a2b1-eeee-460e-b5dc-f5c377d15316",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        -240
      ],
      "parameters": {
        "width": 1800,
        "height": 440,
        "content": "## Cleaning of texts\n**Purpose**: Cleans HTML tags and special characters from the job descriptions for better readability.\nAttempts to parse cleaned job description JSON safely, handling errors gracefully if parsing fails.\nGenerates a human-readable salary string from the salary_min and salary_max fields.\nAdds a high-port flag to any entry where the port number is greater than 5000 (optional diagnostic/extra logic).\nUpserts job data into an Airtable table using the job ID as the unique identifier.\nPurpose: Formats job data into a Telegram-friendly message string.\nPurpose: Sends the formatted message to a specific Telegram chat using a bot.\n\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "3de4886f-f5e4-499b-a425-1ecf91d1c388",
  "connections": {
    "Code5": {
      "main": [
        [
          {
            "node": "RemoteOK Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Remote ok": {
      "main": [
        [
          {
            "node": "Cleaning the received input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text-clean": {
      "main": [
        [
          {
            "node": "Salary to string",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean text1": {
      "main": [
        [
          {
            "node": "Text-clean",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RemoteOK Jobs": {
      "main": [
        [
          {
            "node": "Table to a single message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Salary to string": {
      "main": [
        [
          {
            "node": "Code5",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Remote ok",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Table to a single message": {
      "main": [
        [
          {
            "node": "Telegram1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cleaning the received input": {
      "main": [
        [
          {
            "node": "Clean text1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}