AutomationFlowsWeb Scraping › Score Upwork Jobs and Generate Proposals with Apify, Gpt-4o, Google Sheets…

Score Upwork Jobs and Generate Proposals with Apify, Gpt-4o, Google Sheets…

Original n8n title: Score Upwork Jobs and Generate Proposals with Apify, Gpt-4o, Google Sheets and Telegram

ByNitin Garg @nitin-animoautomation on n8n.io

Schedule Trigger runs every 6 hours (customizable) Apify Scraper fetches Upwork jobs matching your criteria Deduplication filters out jobs you've already seen AI Scoring (GPT-4) evaluates fit, client quality, budget (0-100 score) Filter keeps only jobs scoring 60+ Proposal…

Cron / scheduled trigger★★★★☆ complexityAI-powered26 nodesHTTP RequestGoogle SheetsOpenAITelegramError Trigger
Web Scraping Trigger: Cron / scheduled Nodes: 26 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Error Trigger → Google Sheets 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": "m5K7dzRL7Uo1x9DG",
  "name": "Upwork Job Scraper with AI Scoring - Creator Hub",
  "tags": [],
  "nodes": [
    {
      "id": "5a376579-20a2-4731-8e93-25a9d05c852b",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        32,
        384
      ],
      "parameters": {
        "width": 450,
        "height": 744,
        "content": "## Upwork Job Scraper with AI Scoring\n\nThis workflow automates the process of manual lead sourcing on Upwork. Instead of constantly refreshing the feed, this system identifies high-potential jobs, scores them against your specific expertise, and drafts a personalized proposal.\n\n### How it works\nThe workflow runs on a 6-hour schedule, triggering an Apify actor to scrape the latest job listings based on your keywords. It uses a Google Sheet to cross-reference Job IDs, ensuring you never waste credits processing a lead you've already seen. GPT-4 then evaluates the job description, client history, and budget to assign a Fit Score (0-100). If a job passes your quality threshold (default is 60), AI generates a custom proposal draft, logs the lead to your spreadsheet, and sends a summary to your Telegram bot.\n\n### Setup steps\n1. **Google Sheets:** Create a new spreadsheet with a column header exactly named `Job ID` in the first tab.\n2. **Apify:** Set up an Upwork Scraper Actor and retrieve your Actor ID and API Token.\n3. **Environment Variables:** In n8n, set `GOOGLE_SHEETS_DOC_ID`, `APIFY_ACTOR_ID`, and `TELEGRAM_CHAT_ID`.\n4. **Credentials:** Connect OpenAI (GPT-4 access), Google Sheets, Apify, and Telegram.\n\n### Pro Tip\nThe Generate Proposal node uses GPT-4o-mini to save costs. Update the system prompt with your portfolio links before the first run."
      },
      "typeVersion": 1
    },
    {
      "id": "f8406a75-544a-4f23-bd77-1c04f795df1e",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        512,
        384
      ],
      "parameters": {
        "color": 7,
        "width": 550,
        "height": 302,
        "content": "## Scrape Jobs\nSchedule trigger kicks off Apify to fetch latest Upwork listings.\nBudget ranges (hourly: $25-150, fixed: $200-50k) are customizable."
      },
      "typeVersion": 1
    },
    {
      "id": "e2beaa02-d05e-459b-9aed-3164b9087b6b",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1104,
        384
      ],
      "parameters": {
        "color": 7,
        "width": 450,
        "height": 302,
        "content": "## Deduplicate\nCross-references Google Sheet to skip already-processed jobs"
      },
      "typeVersion": 1
    },
    {
      "id": "321958ec-fe0f-4211-b9ae-ecadfef62c8c",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1600,
        384
      ],
      "parameters": {
        "color": 7,
        "width": 850,
        "height": 420,
        "content": "## Perform AI Scoring\nGPT-4 evaluates fit, client quality, and budget. Filters jobs scoring 60+."
      },
      "typeVersion": 1
    },
    {
      "id": "492b9319-aa17-44d3-84f3-da4bddc0bcb7",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2496,
        384
      ],
      "parameters": {
        "color": 7,
        "width": 886,
        "height": 420,
        "content": "## Generate Proposals\nDrafts a custom proposal based on job description. Review the prompt before first use."
      },
      "typeVersion": 1
    },
    {
      "id": "56ef68e3-eabd-4db9-882a-6c9d177e91e3",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3424,
        384
      ],
      "parameters": {
        "color": 7,
        "width": 430,
        "height": 246,
        "content": "## Log & Notify\nSaves results to Google Sheet and sends Telegram summary"
      },
      "typeVersion": 1
    },
    {
      "id": "e15d6673-338f-4948-9ae0-f35deb7db331",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2736,
        928
      ],
      "parameters": {
        "color": 3,
        "width": 474,
        "height": 270,
        "content": "## Handle Errors\nCatches failures, logs errors, and sends alert to prevent missed opportunities"
      },
      "typeVersion": 1
    },
    {
      "id": "e657cd7b-ecce-4cae-90ed-3ab55f29f259",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        544,
        496
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 6
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "ea4f25c8-46cb-47ac-a0ec-1c9828d28c6c",
      "name": "Run Apify Scraper",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        720,
        496
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/acts/{{ $env.APIFY_ACTOR_ID }}/runs?waitForFinish=300",
        "method": "POST",
        "options": {
          "timeout": 300000
        },
        "jsonBody": "{\n  \"budget.hourlyRate.min\": \"25\",\n  \"budget.hourlyRate.max\": \"150\",\n  \"budget.fixedPrice.min\": \"200\",\n  \"budget.fixedPrice.max\": \"50000\",\n  \"jobCategories\": [\"Web Development\", \"AI Apps & Integration\"],\n  \"limit\": 100\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "836540af-d357-49b6-81b9-bd5158720b25",
      "name": "Get Dataset Items",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        912,
        496
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/datasets/{{ $json.data.defaultDatasetId }}/items?clean=true",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.2
    },
    {
      "id": "f584de90-f5cd-4f26-a8fb-db4fa6129fe0",
      "name": "Read Existing Job IDs",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1184,
        496
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Upwork Jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.GOOGLE_SHEETS_DOC_ID }}"
        }
      },
      "typeVersion": 4.5,
      "alwaysOutputData": true
    },
    {
      "id": "12145b15-ef30-43a5-90a7-dc6a2e76be36",
      "name": "Filter Duplicates",
      "type": "n8n-nodes-base.code",
      "position": [
        1392,
        496
      ],
      "parameters": {
        "jsCode": "const scraped = $('Get Dataset Items').all();\nconst existing = $('Read Existing Job IDs').all();\nconst ids = new Set(existing.map(r => r.json['Job ID']).filter(Boolean));\nconst newJobs = scraped.filter(j => !ids.has(j.json.uid));\nif (!newJobs.length) return [{ json: { _noNewJobs: true } }];\nreturn newJobs;"
      },
      "typeVersion": 2
    },
    {
      "id": "87ee7eae-d29c-449f-8ae8-473b8ffa9311",
      "name": "Has New Jobs?",
      "type": "n8n-nodes-base.if",
      "position": [
        1632,
        496
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.uid }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "0d4df8d1-6d64-436d-9032-95e29a082b84",
      "name": "Normalize Fields",
      "type": "n8n-nodes-base.code",
      "position": [
        1824,
        480
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nreturn items.map(item => {\n  const j = item.json;\n  let budget = j.budget?.hourlyRate?.min ? `$${j.budget.hourlyRate.min}-${j.budget.hourlyRate.max}/hr` : (j.budget?.fixedBudget ? `$${j.budget.fixedBudget}` : 'N/A');\n  return { json: { jobId: j.uid, title: j.title, description: j.description, budget, url: j.externalLink, skills: (j.skills||[]).join(', '), postedAt: j.createdAt, clientVerified: j.client?.paymentVerified ? 'Yes' : 'No', clientSpent: j.client?.totalSpent || 0 }};\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "5d25151e-eb98-48fd-8fc0-7bd666519ae9",
      "name": "AI Scoring",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2000,
        480
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o"
        },
        "options": {},
        "responses": {
          "values": [
            {}
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2
    },
    {
      "id": "86a815ec-d204-446a-8f6c-79819f33e240",
      "name": "Parse AI Score",
      "type": "n8n-nodes-base.code",
      "position": [
        2304,
        480
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst orig = $('Normalize Fields').all();\nreturn items.map((item, i) => {\n  let p = {};\n  try { p = JSON.parse((item.json.message?.content || '{}').replace(/```json?|```/g, '').trim()); } catch(e) {}\n  return { json: { ...orig[i]?.json, score: p.score || 0, decision: p.decision || 'SKIP', reasoning: p.reasoning || '' }};\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "4539cc9c-eb73-42e3-af9a-d0d6228705d9",
      "name": "Filter Score >= 60",
      "type": "n8n-nodes-base.filter",
      "position": [
        2560,
        480
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ $json.score }}",
              "rightValue": 60
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "82d96646-645f-4998-ac00-4f6075b3eb09",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        2752,
        480
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "4efa4a9c-40e5-404f-8378-84bdf63771d8",
      "name": "Generate Proposal",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        2928,
        608
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {},
        "responses": {
          "values": [
            {}
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2
    },
    {
      "id": "502f6c69-f0b4-4f9b-84a4-f27adbb7bd19",
      "name": "Log to Google Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        3216,
        608
      ],
      "parameters": {
        "columns": {
          "mappingMode": "autoMapInputData"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Upwork Jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $env.GOOGLE_SHEETS_DOC_ID }}"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "9b5cfd82-2180-4fef-bfec-0555f316478d",
      "name": "Loop Complete",
      "type": "n8n-nodes-base.noOp",
      "position": [
        2944,
        464
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "04083b94-e4ed-4a60-89ff-ec9a932c5011",
      "name": "Compute Metrics",
      "type": "n8n-nodes-base.code",
      "position": [
        3472,
        464
      ],
      "parameters": {
        "jsCode": "let scraped = 0, passed = 0;\ntry { scraped = $('Get Dataset Items').all().length; } catch(e) {}\ntry { passed = $('Parse AI Score').all().filter(j => j.json.score >= 60).length; } catch(e) {}\nreturn [{ json: { timestamp: new Date().toISOString(), scraped, passed }}];"
      },
      "typeVersion": 2
    },
    {
      "id": "11f2ef02-457d-49e3-b7b5-deaf38a19ced",
      "name": "Send Summary",
      "type": "n8n-nodes-base.telegram",
      "position": [
        3648,
        464
      ],
      "parameters": {
        "text": "=\u2705 Upwork Scraper Done\nScraped: {{ $json.scraped }}\nPassed: {{ $json.passed }}",
        "chatId": "={{ $env.TELEGRAM_CHAT_ID }}",
        "additionalFields": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "c79556bb-509d-4ae3-a49a-c0fc4a977f43",
      "name": "No New Jobs",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1824,
        656
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "9b0e8b7f-b466-472b-9adf-9b6a9efb7ba8",
      "name": "Error Trigger",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        2784,
        1024
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "f70c6e2f-e1a7-4051-b766-c91df40b05e0",
      "name": "Send Error Alert",
      "type": "n8n-nodes-base.telegram",
      "position": [
        2992,
        1024
      ],
      "parameters": {
        "text": "=\ud83d\udea8 Error: {{ $json.error?.message || 'Unknown' }}",
        "chatId": "={{ $env.TELEGRAM_CHAT_ID }}",
        "additionalFields": {}
      },
      "typeVersion": 1.2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "065df9fc-d961-4ff8-8a89-7b9386e8a33f",
  "connections": {
    "AI Scoring": {
      "main": [
        [
          {
            "node": "Parse AI Score",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Error Trigger": {
      "main": [
        [
          {
            "node": "Send Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has New Jobs?": {
      "main": [
        [
          {
            "node": "Normalize Fields",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No New Jobs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Complete": {
      "main": [
        [
          {
            "node": "Compute Metrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Score": {
      "main": [
        [
          {
            "node": "Filter Score >= 60",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compute Metrics": {
      "main": [
        [
          {
            "node": "Send Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Loop Complete",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate Proposal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Fields": {
      "main": [
        [
          {
            "node": "AI Scoring",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Run Apify Scraper",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Duplicates": {
      "main": [
        [
          {
            "node": "Has New Jobs?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Proposal": {
      "main": [
        [
          {
            "node": "Log to Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Dataset Items": {
      "main": [
        [
          {
            "node": "Read Existing Job IDs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Apify Scraper": {
      "main": [
        [
          {
            "node": "Get Dataset Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Score >= 60": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log to Google Sheet": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Existing Job IDs": {
      "main": [
        [
          {
            "node": "Filter Duplicates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

Schedule Trigger runs every 6 hours (customizable) Apify Scraper fetches Upwork jobs matching your criteria Deduplication filters out jobs you've already seen AI Scoring (GPT-4) evaluates fit, client quality, budget (0-100 score) Filter keeps only jobs scoring 60+ Proposal…

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

More Web Scraping workflows → · Browse all categories →

Related workflows

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

Web Scraping

Turn any Amazon India product URL into a fully-edited 10-second lifestyle video and auto-publish it to Instagram, Facebook, X (Twitter), LinkedIn, YouTube, and Threads — with platform-optimized captio

HTTP Request, @Apify/N8N Nodes Apify, OpenRouter Chat +3
Web Scraping

Automate price monitoring for e-commerce competitors—ideal for retailers, analysts, and pricing teams. Scrapes competitor sites, extracts pricing/stock data via AI, detects changes, and sends instant

@Mendable/N8N Nodes Firecrawl, Perplexity, HTTP Request +3
Web Scraping

This workflow is designed for entrepreneurs, sales teams, marketers, and agencies who want to automate lead discovery and build qualified business contact lists — without manual searching or copying d

@Apify/N8N Nodes Apify, Google Sheets, Telegram Trigger +3
Web Scraping

This workflow creates an end-to-end Instagram content pipeline that automatically discovers trending content from competitor channels, extracts valuable insights, and generates new high-quality script

HTTP Request, OpenAI, Google Sheets
Web Scraping

What if your team could skim the best of LinkedIn in 2 minutes instead of scrolling for hours? This workflow transforms raw LinkedIn posts into a bite-sized Slack digest — summarized, grouped, and del

Google Sheets, HTTP Request, Slack +1