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.
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.
How it works β’ Webhook triggers from content creation system in Airtable β’ Downloads media (images/videos) from Airtable URLs β’ Uploads media to Postiz cloud storage β’ Schedules or publishes content a
This n8n workflow receives files sent in a Telegram chat, uploads them to Google Drive, extracts text using OCR (for images and PDFs), and stores the extracted content in Airtable for quick search and
GNCA AI News Pipeline. Uses rssFeedRead, httpRequest, telegram, errorTrigger. Scheduled trigger; 29 nodes.