AutomationFlowsAI & RAG › Auto-post Curated Remote Jobs to Telegram with Browseract and Gemini

Auto-post Curated Remote Jobs to Telegram with Browseract and Gemini

ByMadame AI Team | Kai @madame-ai on n8n.io

This workflow acts as an intelligent job board curator for your Telegram community. It scrapes multiple sources (Remotive, SimplyHired), uses AI to filter out spam and low-quality listings, formats the best jobs into professional posts, and publishes them automatically on a…

Cron / scheduled trigger★★★★☆ complexityAI-powered20 nodesOutput Parser StructuredGoogle Gemini ChatN8N Nodes BrowseractAgentTelegram
AI & RAG Trigger: Cron / scheduled Nodes: 20 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #12436 — we link there as the canonical source.

This workflow follows the Agent → Google Gemini Chat 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 →

Download .json
{
  "id": "2vmW7VCMoHxQAeDi",
  "name": "Auto-post curated remote jobs to Telegram with BrowserAct & Gemini",
  "tags": [],
  "nodes": [
    {
      "id": "72f28ca9-c882-479d-a4d2-53b21ec9b89b",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        2128,
        720
      ],
      "parameters": {
        "autoFix": true,
        "jsonSchemaExample": "[ { \"text\": \"<b>Senior Developer</b>\\n\\n\ud83c\udfe2 <b>Company:</b> TechCorp\\n\ud83d\udcb0 <b>Salary:</b> $150k\\n\\n<b>About:</b> Leading the backend team.\\n\\n\ud83d\udee0 <b>Stack:</b> Java, AWS\\n\\n<a href=http://link.com>\ud83d\udd17 Apply Here</a>\", \"parse_mode\": \"HTML\", \"disable_web_page_preview\": true } ]"
      },
      "typeVersion": 1.3
    },
    {
      "id": "1cc72c30-636a-4364-835e-046612127be4",
      "name": "Google Gemini",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        2000,
        736
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-2.5-pro"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2919d33c-7fef-4311-b8d3-3e746183807a",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        2752,
        512
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "5ff7a89e-0eb9-4497-9aa8-6bb1fda7919b",
      "name": "Fix Output",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        2128,
        848
      ],
      "parameters": {
        "options": {},
        "modelName": "models/gemini-2.5-pro"
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "10f08ee5-52b6-4dba-900d-310f2abae1f5",
      "name": "Scrape Jobs Data (SimplyHired)",
      "type": "n8n-nodes-browseract.browserAct",
      "position": [
        1056,
        432
      ],
      "parameters": {
        "type": "WORKFLOW",
        "timeout": 7200,
        "workflowId": "68979670614019565",
        "workflowConfig": {
          "value": {},
          "schema": [
            {
              "id": "input-Simplyhired",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "description": "If left blank, the default value defined in BrowserAct will be used.",
              "displayName": "Simplyhired",
              "defaultMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "input-Simplyhired"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "open_incognito_mode": false
      },
      "credentials": {
        "browserActApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "002c749a-d8e6-4a42-a309-396fc8b5a680",
      "name": "Scrape Jobs Data (Remotive)",
      "type": "n8n-nodes-browseract.browserAct",
      "position": [
        1056,
        592
      ],
      "parameters": {
        "type": "WORKFLOW",
        "timeout": 7200,
        "workflowId": "68986628591810026",
        "workflowConfig": {
          "value": {},
          "schema": [
            {
              "id": "input-Remotive",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "description": "If left blank, the default value defined in BrowserAct will be used.",
              "displayName": "Remotive",
              "defaultMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "input-Remotive"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "open_incognito_mode": false
      },
      "credentials": {
        "browserActApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "280ecc28-bcf7-41ea-a404-c59a269d9dfb",
      "name": "Splitting Remotive Data",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        1280,
        592
      ],
      "parameters": {
        "jsCode": "// Get the JSON string using the exact path provided by the user.\nconst jsonString = $input.first().json.output.string;\n\nlet parsedData;\n\n// Check if the string exists before attempting to parse\nif (!jsonString) {\n    // Return an empty array or throw an error if no string is found\n    // Throwing an error is usually better to stop the workflow if data is missing.\n    throw new Error(\"Input string is empty or missing at the specified path: $input.first().json.output.string\");\n}\n\ntry {\n    // 1. Parse the JSON string into a JavaScript array of objects\n    parsedData = JSON.parse(jsonString);\n} catch (error) {\n    // Handle JSON parsing errors (e.g., if the string is malformed)\n    throw new Error(`Failed to parse JSON string: ${error.message}`);\n}\n\n// 2. Ensure the parsed data is an array\nif (!Array.isArray(parsedData)) {\n    throw new Error('Parsed data is not an array. It cannot be split into multiple items.');\n}\n\n// 3. Map the array of objects into the n8n item format { json: object }\n// Each element in this array will be treated as a new item by n8n, achieving the split.\nconst outputItems = parsedData.map(item => ({\n    json: item,\n}));\n\n// 4. Return the new array of items\nreturn outputItems;"
      },
      "executeOnce": false,
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "ad7c8b11-d548-45fd-803f-eb8dee119d6e",
      "name": "Splitting SimplyHired Data",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        1280,
        432
      ],
      "parameters": {
        "jsCode": "// Get the JSON string using the exact path provided by the user.\nconst jsonString = $input.first().json.output.string;\n\nlet parsedData;\n\n// Check if the string exists before attempting to parse\nif (!jsonString) {\n    // Return an empty array or throw an error if no string is found\n    // Throwing an error is usually better to stop the workflow if data is missing.\n    throw new Error(\"Input string is empty or missing at the specified path: $input.first().json.output.string\");\n}\n\ntry {\n    // 1. Parse the JSON string into a JavaScript array of objects\n    parsedData = JSON.parse(jsonString);\n} catch (error) {\n    // Handle JSON parsing errors (e.g., if the string is malformed)\n    throw new Error(`Failed to parse JSON string: ${error.message}`);\n}\n\n// 2. Ensure the parsed data is an array\nif (!Array.isArray(parsedData)) {\n    throw new Error('Parsed data is not an array. It cannot be split into multiple items.');\n}\n\n// 3. Map the array of objects into the n8n item format { json: object }\n// Each element in this array will be treated as a new item by n8n, achieving the split.\nconst outputItems = parsedData.map(item => ({\n    json: item,\n}));\n\n// 4. Return the new array of items\nreturn outputItems;"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "41a99712-6ddf-4a9a-a650-7d60900e74f6",
      "name": "Wait for Both Path Outputs",
      "type": "n8n-nodes-base.merge",
      "position": [
        1488,
        512
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "175503ea-1a18-44ed-aa1d-c2d8fa347850",
      "name": "Merge Branch Outputs",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1840,
        512
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "5587035c-0262-4ffc-9435-89744d01ba8f",
      "name": "Analyze Job Data & Generate Response",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        2000,
        512
      ],
      "parameters": {
        "text": "={{ $json.data }}",
        "options": {
          "systemMessage": "You are an expert Job Curator and Social Media Manager for a high-quality professional Telegram channel. Your task is to process a raw JSON array of job listings, filter out low-quality/spam entries, and format the valid jobs into a Telegram-ready JSON response.\n\n1. Filtering Rules (Strictly Apply These):\n\nDeduplicate: If a job appears multiple times (e.g., once with just a link and once with a description), only keep the version with the full_job_description. Discard the others.\n\nRemove Low Quality: Discard entries that have no full_job_description or empty descriptions.\n\nRemove Spam/Gig-Work: Discard roles that appear to be low-tier \"gig\" work, content mills, or surveys (e.g., \"IAPWE\", \"Omni Interactions\", \"Student Representatives\", or generic \"Freelance Writer\" roles with no clear company identity).\n\nSalary Threshold: If a salary is listed as under $20/hour, discard it (unless it is a clear internship at a reputable tech company).\n\n2. Formatting Rules:\n\nOutput Structure: You must output a single JSON array of objects. Each object must have the keys: text, parse_mode, and disable_web_page_preview.\n\nParse Mode: Set parse_mode to \"HTML\".\n\nAllowed Tags: Use only the following HTML tags supported by Telegram: <b>, <i>, <a>, <code>. Do NOT use Markdown (**, __, [ ]).\n\nContent Layout:\n\nHeadline: <b>Job Title</b>\n\nMeta: \ud83c\udfe2 Company: [Name] | \ud83d\udcb0 Salary: [Value] | \ud83d\udccd Location: [Value]\n\nSummary: A 1-2 sentence professional summary of the role.\n\nTags: \ud83d\udee0 Stack/Skills: [List 3-5 key skills]\n\nCTA: <a href=\"job_link\">\ud83d\udd17 Apply Here</a>\n\n3. Output Constraints:\n\nNo Markdown Blocks: Do not output code blocks (like ```json). Output raw text starting with [ and ending with ].\n\nJSON Validity: Ensure the output is valid, parsable JSON. Escape all special characters (like quotes) inside the JSON strings correctly.\n\nExample Output Format: [ { \"text\": \"<b>Senior Developer</b>\\n\\n\ud83c\udfe2 <b>Company:</b> TechCorp\\n\ud83d\udcb0 <b>Salary:</b> $150k\\n\\n<b>About:</b> Leading the backend team.\\n\\n\ud83d\udee0 <b>Stack:</b> Java, AWS\\n\\n<a href=\"http://link.com\">\ud83d\udd17 Apply Here</a>\", \"parse_mode\": \"HTML\", \"disable_web_page_preview\": true } ]"
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3
    },
    {
      "id": "8f1020f5-850e-4a66-85c1-e77c2f54da17",
      "name": "Split Generated Job Items",
      "type": "n8n-nodes-base.code",
      "position": [
        2544,
        512
      ],
      "parameters": {
        "jsCode": "// 1. Get the data using the path from your first message\n// We access .output directly because your data shows it as an array, not a string inside .string\nconst rawData = $input.first().json.output;\n\nlet parsedData;\n\n// Check if the data exists before proceeding\nif (!rawData) {\n    // Throw an error to stop the workflow if data is missing\n    throw new Error(\"Input is empty or missing at the specified path: $input.first().json.output\");\n}\n\n// 2. Handle the data format\n// If n8n has already parsed it as an array (standard behavior), we use it directly.\n// If it happens to be a string (edge case), we parse it.\nif (typeof rawData === 'string') {\n    try {\n        parsedData = JSON.parse(rawData);\n    } catch (error) {\n        throw new Error(`Failed to parse JSON string: ${error.message}`);\n    }\n} else {\n    parsedData = rawData;\n}\n\n// 3. Ensure the data is an array\nif (!Array.isArray(parsedData)) {\n    throw new Error('The data at json.output is not an array. It cannot be split into multiple items.');\n}\n\n// 4. Map the array of objects into the n8n item format { json: object }\n// Each element in this array will be treated as a new item by n8n\nconst outputItems = parsedData.map(item => ({\n    json: item,\n}));\n\n// 5. Return the new array of items\nreturn outputItems;"
      },
      "typeVersion": 2
    },
    {
      "id": "80104868-b954-43f9-ad18-38642658474f",
      "name": "Avoid Rate Limits",
      "type": "n8n-nodes-base.wait",
      "position": [
        2960,
        480
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "c5c416dd-e13c-42b7-9aaf-b6b612da92cb",
      "name": "Send Travel List to User Channel",
      "type": "n8n-nodes-base.telegram",
      "position": [
        3168,
        512
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "chatId": "parameters.chatId==@Channel ID (Use Channel ID or Chat ID)",
        "additionalFields": {
          "parse_mode": "={{ $json.parse_mode }}",
          "disable_web_page_preview": "={{ $json.disable_web_page_preview }}"
        }
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "e9b06a2a-1ae8-42d4-8922-abadece65997",
      "name": "Schedule Daily",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        832,
        512
      ],
      "parameters": {
        "rule": {
          "interval": [
            {}
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "c0a06e5a-f431-4b10-82d5-a7c03aa6c5f6",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        832,
        -144
      ],
      "parameters": {
        "color": 6,
        "width": 672,
        "height": 384,
        "content": "@[youtube](DEBF0ILrM5E)"
      },
      "typeVersion": 1
    },
    {
      "id": "d9933a15-6076-40e4-87eb-460ec5c4751f",
      "name": "Documentation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        -144
      ],
      "parameters": {
        "width": 380,
        "height": 520,
        "content": "## \u26a1 Workflow Overview & Setup\n\n**Summary:** This automation daily scrapes remote job listings from SimplyHired and Remotive using BrowserAct, uses AI to filter spam and curate high-quality roles, and auto-posts them to a Telegram channel.\n\n### Requirements\n* **Credentials:** BrowserAct, Google Gemini (PaLM), Telegram.\n* **Mandatory:** BrowserAct API (Template: **Automated Remote Job Fetching & Filtering for Telegram Feed**)\n\n### How to Use\n1. **Credentials:** Set up your BrowserAct, Google Gemini, and Telegram Bot API keys in n8n.\n2. **BrowserAct Template:** Ensure you have the **Automated Remote Job Fetching & Filtering for Telegram Feed** template saved in your BrowserAct account.\n3. **Configuration:** Update the Telegram node with your target Channel ID (e.g., `@yourchannel`).\n\n### Need Help?\n[How to Find Your BrowserAct API Key & Workflow ID](https://docs.browseract.com)\n[How to Connect n8n to BrowserAct](https://docs.browseract.com)\n[How to Use & Customize BrowserAct Templates](https://docs.browseract.com)"
      },
      "typeVersion": 1
    },
    {
      "id": "53de33d7-4c6c-498d-bbad-170c2426dd46",
      "name": "Step 1 Explanation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        832,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 764,
        "height": 124,
        "content": "### \ud83d\udd75\ufe0f Step 1: Multi-Source Scraping\n\nThe workflow triggers daily to run dual BrowserAct sessions, scraping the latest remote job listings from SimplyHired and Remotive. The raw data from both sources is then parsed into individual job items for processing."
      },
      "typeVersion": 1
    },
    {
      "id": "25aeb140-51d0-4858-826e-73f78bc89056",
      "name": "Step 2 Explanation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1680,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 780,
        "height": 124,
        "content": "### \ud83e\udde0 Step 2: AI Curation & Quality Control\n\nAn AI agent aggregates the job feeds, performing deduplication and strict filtering. It discards low-tier gig work, spam, and roles below a specific salary threshold while generating professional HTML-formatted posts for Telegram."
      },
      "typeVersion": 1
    },
    {
      "id": "eb6db87d-a37f-4850-a86f-074f9803592c",
      "name": "Step 3 Explanation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2544,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 776,
        "height": 124,
        "content": "### \ud83d\ude80 Step 3: Throttled Delivery\n\nThe curated job list is split into individual items and sent sequentially to the Telegram channel. A wait node is included between posts to respect Telegram's rate limits and ensure consistent delivery."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "a0056f52-2c96-4117-8150-473d54edcf8c",
  "connections": {
    "Fix Output": {
      "ai_languageModel": [
        [
          {
            "node": "Structured Output Parser",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini": {
      "ai_languageModel": [
        [
          {
            "node": "Analyze Job Data & Generate Response",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Daily": {
      "main": [
        [
          {
            "node": "Scrape Jobs Data (Remotive)",
            "type": "main",
            "index": 0
          },
          {
            "node": "Scrape Jobs Data (SimplyHired)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Avoid Rate Limits",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Avoid Rate Limits": {
      "main": [
        [
          {
            "node": "Send Travel List to User Channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Branch Outputs": {
      "main": [
        [
          {
            "node": "Analyze Job Data & Generate Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Splitting Remotive Data": {
      "main": [
        [
          {
            "node": "Wait for Both Path Outputs",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Analyze Job Data & Generate Response",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Split Generated Job Items": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Splitting SimplyHired Data": {
      "main": [
        [
          {
            "node": "Wait for Both Path Outputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Both Path Outputs": {
      "main": [
        [
          {
            "node": "Merge Branch Outputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Jobs Data (Remotive)": {
      "main": [
        [
          {
            "node": "Splitting Remotive Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Jobs Data (SimplyHired)": {
      "main": [
        [
          {
            "node": "Splitting SimplyHired Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Travel List to User Channel": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Job Data & Generate Response": {
      "main": [
        [
          {
            "node": "Split Generated Job Items",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow acts as an intelligent job board curator for your Telegram community. It scrapes multiple sources (Remotive, SimplyHired), uses AI to filter out spam and low-quality listings, formats the best jobs into professional posts, and publishes them automatically on a…

Source: https://n8n.io/workflows/12436/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

This workflow automates the creation of engaging podcast summaries for your Telegram channel. It scrapes a podcast feed (like Apple Podcasts Top Charts), uses AI to format the episodes into a clean, r

Google Gemini Chat, Output Parser Structured, N8N Nodes Browseract +2
AI & RAG

Author: Nguyen Thieu Toan Category: Community & Knowledge Automation Tags: Telegram, Reddit, n8n Forum, AI Summarization, Gemini, Groq

Groq Chat, Output Parser Structured, Memory Mongo Db Chat +5
AI & RAG

This workflow transforms your Telegram bot into an intelligent creative assistant. It can chat conversationally, fetch trending image prompts from PromptHero for inspiration, or perform a deep "remix"

Telegram Trigger, Output Parser Structured, Telegram +6
AI & RAG

This workflow acts as an AI-powered "Viral Architect" for YouTube creators. Simply send a video topic (e.g., "Kling 2.6") to your Telegram bot, and it will scrape top-performing competitor thumbnails,

Output Parser Structured, N8N Nodes Browseract, OpenRouter Chat +6
AI & RAG

This workflow acts as an intelligent content engine. Simply send a link to your Telegram bot (e.g., a product page or news article), and it will automatically scrape the content, rewrite it into a hig

Google Gemini, Agent, OpenRouter Chat +7