AutomationFlowsAI & RAG › Write Upwork Proposals From Vollna Alerts Using Claude, Gmail and Sheets

Write Upwork Proposals From Vollna Alerts Using Claude, Gmail and Sheets

ByAkshay Chug @akshaychug on n8n.io

Stop spending 20 minutes writing each Upwork proposal from scratch. This workflow reads your Vollna job alert emails, scores every job against your skills and budget preferences, and uses Claude to write a personalised 55-75 word cover letter for every match and saved as a Gmail…

Event trigger★★★★☆ complexityAI-powered17 nodesGmail TriggerChain LlmAnthropic ChatGmailSlackGoogle Sheets
AI & RAG Trigger: Event Nodes: 17 Complexity: ★★★★☆ AI nodes: yes Added:

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

This workflow follows the Chainllm → Gmail 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": "tl2kDLfcy3z7xsOc",
  "name": "AI Upwork Proposal Writer: Auto-Generate Cover Letters from Vollna Job Alerts using Claude",
  "tags": [],
  "nodes": [
    {
      "id": "64a1ff1c-ba80-4f11-812c-ed626d984bf6",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        -64
      ],
      "parameters": {
        "color": 0,
        "width": 480,
        "height": 1092,
        "content": "## AI Upwork Proposal Writer using Vollna and Claude\n\nStop spending 20 minutes writing each Upwork proposal from scratch. This workflow reads your Vollna job alert emails, scores each job against your skills and profile, and uses Claude to write a punchy 55-75 word cover letter for every match -- saved as a Gmail draft ready to review and send in one click.\n\n### How it works\n\n1. Every 30 minutes the workflow checks Gmail for new Vollna job alert emails.\n2. A Code node parses the email and extracts every individual job title, URL, and budget from the Vollna HTML table format.\n3. Each job is scored 1-10 against your profile -- skills, preferred budget, and job filters you configure in the Settings node.\n4. Jobs scoring below your threshold are skipped and logged as Not a Fit.\n5. For every qualifying job, Claude Haiku reads the job title and your profile, then writes a personalised 55-75 word cover letter following the Nick Saraev formula: hook with relevant experience, proof point, specific offer, soft call to action.\n6. The proposal is saved as a Gmail draft with the job title as the subject -- ready for your review before sending.\n7. Every job processed -- matched or skipped -- is logged to Google Sheets with the score, reasoning, and draft status.\n\n### Setup steps\n\n- [ ] **Vollna** -- Make sure you have a Vollna account and at least one active filter sending job alerts to your Gmail. The workflow reads emails from vollna.com automatically.\n- [ ] **Gmail trigger** -- Connect your Gmail account in the Check for Vollna Alerts node.\n- [ ] **Settings node** -- Open Configure Profile and Settings and fill in your name, skills, hourly rate, minimum budget, score threshold, and a short bio. This is the only node you need to personalise.\n- [ ] **Claude AI** -- Click the Claude Haiku sub-node under Write Proposal with Claude, add a new Anthropic credential, and paste your API key from console.anthropic.com. Haiku is used because it is fast and cheap -- fractions of a cent per proposal.\n- [ ] **Gmail drafts** -- Connect your Gmail account in the Save Proposal as Draft node.\n- [ ] **Google Sheets** -- Create a sheet with columns: Timestamp, Job Title, Budget, Score, Status, Draft Saved, Job URL. Connect your Google account in Log Job to Sheets.\n- [x] Activate the workflow. Every new Vollna email will be processed automatically.\n\n### Customization\n\nRaise or lower the SCORE_THRESHOLD in the Settings node to control how selective the workflow is. Edit the Claude prompt in Write Proposal with Claude to match your personal writing style. Add a Slack node after Save Proposal as Draft to get a ping every time a new draft is ready."
      },
      "typeVersion": 1
    },
    {
      "id": "99284132-49d3-4300-a927-8524c028d473",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 420,
        "height": 456,
        "content": "## Fetch and parse Vollna alerts\n\nPolls Gmail every 30 minutes for emails from vollna.com. A Code node strips the HTML and extracts each individual job posting -- title, budget, and URL -- from the Vollna email table format."
      },
      "typeVersion": 1
    },
    {
      "id": "2a1df886-f669-4ca5-aeec-035dce3f1ad8",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 392,
        "height": 456,
        "content": "## Score and filter jobs\n\nEach job is scored 1-10 against your profile using a Code node. Jobs below your threshold are skipped. Edit the Settings node to set your skills, minimum budget, and score threshold -- no other node needs changing."
      },
      "typeVersion": 1
    },
    {
      "id": "a04d329d-d905-408c-b8f6-3bdcdb9877c8",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1584,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 572,
        "height": 808,
        "content": "## Write proposal and save draft\n\nClaude Haiku writes a 55-75 word cover letter for each qualifying job using the Nick Saraev formula: hook with relevant experience, one proof point, specific offer, soft call to action. The proposal is saved as a Gmail draft with the job title as the subject.\n\n> Slack node is optional -- right-click and Disable if unused."
      },
      "typeVersion": 1
    },
    {
      "id": "2eb67787-b96e-4064-ac62-fe7ecc0370be",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2256,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 724,
        "height": 360,
        "content": "## Log every job to Sheets\n\nEvery job processed -- matched or skipped -- is logged to Google Sheets with the score, Claude reasoning, budget, and whether a draft was saved. Gives you a full record of your pipeline."
      },
      "typeVersion": 1
    },
    {
      "id": "c3c0edd3-9911-491c-810f-4bf571cad2ac",
      "name": "Check for Vollna Alerts",
      "type": "n8n-nodes-base.gmailTrigger",
      "position": [
        672,
        208
      ],
      "parameters": {
        "filters": {
          "sender": "user@example.com",
          "readStatus": "unread"
        },
        "pollTimes": {
          "item": [
            {
              "mode": "everyX",
              "unit": "minutes",
              "value": 30
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "8e5c8818-85e3-42d2-ab2b-46833fe341dc",
      "name": "Configure Profile and Settings",
      "type": "n8n-nodes-base.code",
      "position": [
        912,
        208
      ],
      "parameters": {
        "jsCode": "// ================================================\n//  EDIT THIS NODE -- all your settings live here\n// ================================================\n\nconst YOUR_NAME         = 'Your Name';\nconst YOUR_SKILLS       = 'n8n, Make.com, Claude API, Zapier, AI agents, workflow automation';\nconst YOUR_BIO          = 'I build AI-powered automations for B2B businesses. Recent work includes a hiring automation that screened 230 applications at 95% accuracy and a cold email reply classifier used in production.';\nconst YOUR_HOURLY_RATE  = 30;      // USD -- used for scoring budget fit\nconst MIN_BUDGET_FIXED  = 75;     // Skip fixed-price jobs below this\nconst MIN_BUDGET_HOURLY = 15;     // Skip hourly jobs below this\nconst SCORE_THRESHOLD   = 6;      // Only write proposals for jobs scoring 6 or above (out of 10)\nconst SKIP_KEYWORDS     = ['expert level', 'senior only', 'agency only', 'wordpress', 'shopify theme'];\n\n// ================================================\n//  DO NOT EDIT BELOW THIS LINE\n// ================================================\n\nconst email = $input.first().json;\nconst rawHtml = email.html || email.body || '';\nconst subject = email.subject || '';\nconst timestamp = new Date().toISOString();\n\nreturn [{\n  json: {\n    your_name:         YOUR_NAME,\n    your_skills:       YOUR_SKILLS,\n    your_bio:          YOUR_BIO,\n    your_hourly_rate:  YOUR_HOURLY_RATE,\n    min_budget_fixed:  MIN_BUDGET_FIXED,\n    min_budget_hourly: MIN_BUDGET_HOURLY,\n    score_threshold:   SCORE_THRESHOLD,\n    skip_keywords:     SKIP_KEYWORDS,\n    raw_html:          rawHtml,\n    email_subject:     subject,\n    timestamp:         timestamp\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "6858bace-eeb4-4f16-b756-59f2e5dcc1ca",
      "name": "Score Job Against Profile",
      "type": "n8n-nodes-base.code",
      "position": [
        1392,
        208
      ],
      "parameters": {
        "jsCode": "// Score this job 1-10 against the freelancer profile\nconst job = $input.first().json;\n\n// Skip if no jobs found in email\nif (job._no_jobs) {\n  return [{ json: { ...job, score: 0, score_reasoning: 'No jobs found in email', status: 'SKIPPED' } }];\n}\n\nconst title = (job.title || '').toLowerCase();\nconst budgetText = (job.budget_text || '').toLowerCase();\nconst budgetType = (job.budget_type || '').toLowerCase();\nconst skills = (job.your_skills || '').toLowerCase();\nconst skipKeywords = job.skip_keywords || [];\n\nlet score = 5; // start neutral\nlet reasons = [];\n\n// Check skip keywords first\nfor (const kw of skipKeywords) {\n  if (title.includes(kw.toLowerCase())) {\n    return [{ json: {\n      ...job,\n      score: 0,\n      score_reasoning: 'Contains skip keyword: ' + kw,\n      status: 'SKIPPED'\n    }}];\n  }\n}\n\n// Score: skill match\nconst skillList = skills.split(',').map(s => s.trim());\nlet skillMatches = 0;\nfor (const skill of skillList) {\n  if (skill.length > 2 && title.includes(skill)) skillMatches++;\n}\nif (skillMatches >= 2) { score += 2; reasons.push('strong skill match'); }\nelse if (skillMatches === 1) { score += 1; reasons.push('partial skill match'); }\nelse { score -= 1; reasons.push('low skill match'); }\n\n// Score: budget\nconst budgetNum = parseFloat(budgetText.replace(/[^0-9.]/g, '')) || 0;\nconst isHourly = budgetType.includes('hourly');\nconst isFixed = budgetType.includes('fixed');\nconst minFixed = job.min_budget_fixed || 75;\nconst minHourly = job.min_budget_hourly || 15;\n\nif (isFixed && budgetNum > 0 && budgetNum < minFixed) {\n  score -= 2; reasons.push('budget below minimum');\n} else if (isHourly && budgetNum > 0 && budgetNum < minHourly) {\n  score -= 2; reasons.push('rate below minimum');\n} else if (budgetNum === 0 || budgetText === 'not specified') {\n  score -= 1; reasons.push('budget not specified');\n} else if (isFixed && budgetNum >= 300) {\n  score += 2; reasons.push('high budget');\n} else if (isHourly && budgetNum >= job.your_hourly_rate) {\n  score += 2; reasons.push('good rate');\n} else {\n  score += 1; reasons.push('acceptable budget');\n}\n\n// Score: automation/AI keywords in title\nconst hotKeywords = ['n8n', 'make.com', 'zapier', 'claude', 'ai agent', 'automation', 'workflow', 'openai', 'chatgpt', 'anthropic', 'gohighlevel', 'ghl'];\nfor (const kw of hotKeywords) {\n  if (title.includes(kw)) { score += 1; reasons.push('hot keyword: ' + kw); break; }\n}\n\n// Clamp score\nscore = Math.max(1, Math.min(10, score));\n\nconst threshold = job.score_threshold || 6;\nconst status = score >= threshold ? 'MATCH' : 'BELOW_THRESHOLD';\n\nreturn [{ json: {\n  ...job,\n  score,\n  score_reasoning: reasons.join(', '),\n  status\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "f5d4f9fe-8a4c-4f6e-b711-7d94cf60e103",
      "name": "Is It a Match",
      "type": "n8n-nodes-base.if",
      "position": [
        1632,
        208
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c0883937-3578-4096-a4a4-ac0a4aeef807",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "MATCH"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "a42f7772-338a-4fee-a65c-318b65a67a2f",
      "name": "Write Proposal with Claude",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        1872,
        80
      ],
      "parameters": {
        "text": "=You are an expert Upwork freelancer writing a cover letter for a job posting.\n\nYOUR PROFILE:\nName: {{ $json.your_name }}\nSkills: {{ $json.your_skills }}\nBio: {{ $json.your_bio }}\n\nJOB:\nTitle: {{ $json.title }}\nBudget: {{ $json.budget_text }} {{ $json.budget_type }}\n\nWrite a cover letter following this exact formula (Nick Saraev style):\n1. Open with one sentence showing you do this exact thing regularly\n2. Add one specific proof point from the bio (number, outcome, or tool)\n3. State one specific thing you would do for this client\n4. End with a soft question or call to action\n\nSTRICT RULES:\n- Total length: 55 to 75 words. Count carefully.\n- No buzzwords: no \"passionate\", \"leverage\", \"synergy\", \"excited to apply\"\n- No generic openers like \"I saw your job posting\" or \"I am interested\"\n- Sound like a human expert, not an AI\n- Do not use any formatting, bullet points, or headers\n- Plain paragraph only\n- Do not mention the word \"proposal\" or \"cover letter\"\n\nWrite the cover letter now. Nothing else -- no preamble, no explanation.",
        "promptType": "define"
      },
      "typeVersion": 1.4
    },
    {
      "id": "c6f66df9-3531-4afc-996f-603a4b7a1a0d",
      "name": "Claude Haiku",
      "type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
      "position": [
        1872,
        288
      ],
      "parameters": {
        "model": "claude-haiku-4-5-20251001",
        "options": {
          "temperature": 0.7
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "f22e31fc-6725-44c5-b0f5-b45bcf677700",
      "name": "Extract Proposal Text",
      "type": "n8n-nodes-base.set",
      "position": [
        2000,
        224
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "64c7ec20-14f2-427d-98c8-96d6f48a55c5",
              "name": "proposal_text",
              "type": "string",
              "value": "={{ $json.text.trim() }}"
            },
            {
              "id": "b35ef786-bd0e-4df7-b206-5471afa0144d",
              "name": "title",
              "type": "string",
              "value": "={{ $('Score Job Against Profile').item.json.title }}"
            },
            {
              "id": "b1125a9b-1321-4a85-9572-519ebd562c16",
              "name": "job_url",
              "type": "string",
              "value": "={{ $('Score Job Against Profile').item.json.job_url }}"
            },
            {
              "id": "516e6a6c-d08d-4efe-aae4-076c646702f7",
              "name": "budget_text",
              "type": "string",
              "value": "={{ $('Score Job Against Profile').item.json.budget_text }}"
            },
            {
              "id": "edca4e8f-9e78-453a-9ad2-f1b15233fe2b",
              "name": "budget_type",
              "type": "string",
              "value": "={{ $('Score Job Against Profile').item.json.budget_type }}"
            },
            {
              "id": "0d1ed499-0fc6-4e01-903c-dabeece27b41",
              "name": "score",
              "type": "number",
              "value": "={{ $('Score Job Against Profile').item.json.score }}"
            },
            {
              "id": "12215cff-ba78-40d2-9369-64e243bd04e4",
              "name": "score_reasoning",
              "type": "string",
              "value": "={{ $('Score Job Against Profile').item.json.score_reasoning }}"
            },
            {
              "id": "55460a2b-a268-4128-a68f-550a07f1a8a8",
              "name": "timestamp",
              "type": "string",
              "value": "={{ $('Score Job Against Profile').item.json.timestamp }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a368613e-4bf0-4041-a419-8107c9398121",
      "name": "Save Proposal as Draft",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2304,
        80
      ],
      "parameters": {
        "operation": "createDraft"
      },
      "typeVersion": 2.1
    },
    {
      "id": "63cfea6b-7ecf-4b5b-966a-8dd3d1f88d7c",
      "name": "Notify New Draft - Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        2528,
        80
      ],
      "parameters": {
        "text": "New proposal draft ready!\n\nJob: {{ $json.title }}\nBudget: {{ $json.budget_text }} {{ $json.budget_type }}\nScore: {{ $json.score }}/10\n\nReview it in Gmail drafts.",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "#upwork-proposals"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.2
    },
    {
      "id": "0cca83d1-8d20-46a0-8190-c8ab7411d2f7",
      "name": "Log Job to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2752,
        80
      ],
      "parameters": {
        "columns": {
          "value": {
            "Score": "={{ $json.score }}",
            "Budget": "={{ $json.budget_text + ' ' + $json.budget_type }}",
            "Status": "Proposal Drafted",
            "Job URL": "={{ $json.job_url }}",
            "Job Title": "={{ $json.title }}",
            "Timestamp": "={{ $json.timestamp }}",
            "Draft Saved": "Yes"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Upwork Jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "d3d7f9af-c98e-4b9b-b9b4-be7d51ec7a78",
      "name": "Log Skipped Job to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1824,
        480
      ],
      "parameters": {
        "columns": {
          "value": {
            "Score": "={{ $json.score }}",
            "Budget": "={{ $json.budget_text + ' ' + $json.budget_type }}",
            "Status": "={{ $json.status }}",
            "Job URL": "={{ $json.job_url }}",
            "Job Title": "={{ $json.title }}",
            "Timestamp": "={{ $json.timestamp }}",
            "Draft Saved": "No"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Upwork Jobs"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "e7fcaca8-8bd8-41f0-8bee-100db9907de3",
      "name": "Parse Jobs from Email",
      "type": "n8n-nodes-base.code",
      "position": [
        1152,
        208
      ],
      "parameters": {
        "jsCode": "// Parse individual job listings from Vollna HTML email\nconst data = $input.first().json;\nconst html = data.raw_html || '';\n\n// Extract job rows from the Vollna table format\n// Vollna emails use <span style=\"text-decoration: underline;\">TITLE</span> inside <a> tags\n// and budget appears as <strong style=\"color: #f15d65;\">AMOUNT</strong>\n\nconst jobs = [];\n\n// Match each table row containing a job\nconst rowPattern = /<tr[^>]*style=\"border-bottom:1px solid #ecedee;\"[sS]*?</tr>/gi;\nconst rows = html.match(rowPattern) || [];\n\nfor (const row of rows) {\n  // Extract job title\n  const titleMatch = row.match(/<span style=\"text-decoration: underline;\">([sS]*?)</span>/i);\n  const title = titleMatch ? titleMatch[1].replace(/<[^>]+>/g, '').trim() : '';\n\n  // Extract job URL\n  const urlMatch = row.match(/href=\"[^\"]*url=([^\"&]+)/i);\n  let jobUrl = '';\n  if (urlMatch) {\n    try { jobUrl = decodeURIComponent(decodeURIComponent(urlMatch[1])); } catch(e) { jobUrl = urlMatch[1]; }\n  }\n\n  // Extract budget amount\n  const budgetStrongMatch = row.match(/<strong[^>]*color: #f15d65[^>]*>([sS]*?)</strong>/i);\n  const budgetText = budgetStrongMatch ? budgetStrongMatch[1].replace(/<[^>]+>/g, '').trim() : 'not specified';\n\n  // Extract budget type (Fixed or Hourly)\n  const budgetTypeMatch = row.match(/<small[^>]*color: #666666[^>]*>([sS]*?)</small>/i);\n  const budgetType = budgetTypeMatch ? budgetTypeMatch[1].replace(/<[^>]+>/g, '').trim() : '';\n\n  // Extract published date\n  const dateMatch = row.match(/(d{2}/d{2}/d{4}s+d{1,2}:d{2}s+[AP]M)/i);\n  const publishedAt = dateMatch ? dateMatch[1] : '';\n\n  if (title && title.length > 3) {\n    jobs.push({\n      title,\n      job_url: jobUrl,\n      budget_text: budgetText,\n      budget_type: budgetType,\n      published_at: publishedAt,\n      // Pass through profile settings\n      your_name:         data.your_name,\n      your_skills:       data.your_skills,\n      your_bio:          data.your_bio,\n      your_hourly_rate:  data.your_hourly_rate,\n      min_budget_fixed:  data.min_budget_fixed,\n      min_budget_hourly: data.min_budget_hourly,\n      score_threshold:   data.score_threshold,\n      skip_keywords:     data.skip_keywords,\n      timestamp:         data.timestamp\n    });\n  }\n}\n\nif (jobs.length === 0) {\n  return [{ json: { _no_jobs: true, timestamp: data.timestamp } }];\n}\n\nreturn jobs.map(j => ({ json: j }));"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "28ea905c-101e-48db-84a7-14032fa0d0b3",
  "connections": {
    "Claude Haiku": {
      "ai_languageModel": [
        [
          {
            "node": "Write Proposal with Claude",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Is It a Match": {
      "main": [
        [
          {
            "node": "Write Proposal with Claude",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Skipped Job to Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Proposal Text": {
      "main": [
        [
          {
            "node": "Save Proposal as Draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Jobs from Email": {
      "main": [
        [
          {
            "node": "Score Job Against Profile",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Proposal as Draft": {
      "main": [
        [
          {
            "node": "Notify New Draft - Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for Vollna Alerts": {
      "main": [
        [
          {
            "node": "Configure Profile and Settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Notify New Draft - Slack": {
      "main": [
        [
          {
            "node": "Log Job to Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Score Job Against Profile": {
      "main": [
        [
          {
            "node": "Is It a Match",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Proposal with Claude": {
      "main": [
        [
          {
            "node": "Extract Proposal Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure Profile and Settings": {
      "main": [
        [
          {
            "node": "Parse Jobs from Email",
            "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

Stop spending 20 minutes writing each Upwork proposal from scratch. This workflow reads your Vollna job alert emails, scores every job against your skills and budget preferences, and uses Claude to write a personalised 55-75 word cover letter for every match and saved as a Gmail…

Source: https://n8n.io/workflows/15789/ — 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

Your inbox shouldn't run your day. This workflow checks Gmail every 15 minutes, uses Claude AI to classify every new email into Urgent, Needs Reply, FYI Only, Automated, or Spam — then takes the right

Gmail Trigger, Chain Llm, Anthropic Chat +3
AI & RAG

Stop wasting time on leads that will never convert. This workflow scores every inbound form submission 1-10 using Claude AI, then automatically replies and routes based on fit — hot leads get an insta

Form Trigger, Chain Llm, Anthropic Chat +3
AI & RAG

Email-Automation-Template. Uses lmChatAnthropic, microsoftOutlookTrigger, gmail, chainLlm. Event-driven trigger; 38 nodes.

Anthropic Chat, Microsoft Outlook Trigger, Gmail +4
AI & RAG

This workflow automates the full lifecycle of a vehicle insurance claim — from an incoming Gmail email to a signed, watermarked PDF decision letter delivered back to the claimant.

Gmail Trigger, N8N Nodes Pdf Api Hub, Google Sheets +4
AI & RAG

Automate your lead intake, scoring, and outreach pipeline. This workflow collects leads from forms, enriches and scores them using Relevance AI, routes them by quality, and triggers the right follow-u

Form Trigger, HTTP Request, Chain Llm +6