This workflow corresponds to n8n.io template #4383 β we link there as the canonical source.
This workflow follows the Airtable β HTTP Request recipe pattern β see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide β
{
"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(///g, '/');\n text = text.replace(/'/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
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing β you'll add your own.
airtableTokenApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
π Remote Job Automation Workflow Automatically fetch, clean, and broadcast the latest remote job listings β powered by RemoteOK, Airtable, and Telegram.
Source: https://n8n.io/workflows/4383/ β original creator credit. Request a take-down β
More Slack & Telegram workflows β Β· Browse all categories β
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
01 - Intake (F1 + F2). Uses airtable, httpRequest, telegram. Scheduled trigger; 19 nodes.
Send A Random Recipe Once A Day To Telegram. Uses airtable, telegram, httpRequest, telegramTrigger. Scheduled trigger; 15 nodes.
This telegram bot is designed to send one random recipe a day.
Auto Hunt. Uses httpRequest, googleSheets, rssFeedRead, telegram. Scheduled trigger; 78 nodes.
Auto-Hunt. Uses httpRequest, googleSheets, rssFeedRead, telegram. Scheduled trigger; 78 nodes.