{
  "id": "tnztcvK5fVFSz3X4M56OF",
  "name": "Automate proposals on Upwork with AI, Airtable and Slack",
  "tags": [],
  "nodes": [
    {
      "id": "468b4807-7f16-4699-be7d-9cfe32749dee",
      "name": "RSS Feed - n8n & Automation",
      "type": "n8n-nodes-base.rssFeedReadTrigger",
      "position": [
        -16,
        160
      ],
      "parameters": {
        "feedUrl": "YOUR_VOLLNA_RSS_FEED_URL",
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "abd092d6-1bb3-474a-96a8-a37ca89541f9",
      "name": "Filter: Skills Match",
      "type": "n8n-nodes-base.code",
      "position": [
        496,
        160
      ],
      "parameters": {
        "jsCode": "const YOUR_SKILLS = [\n  'n8n', 'automation', 'workflow', 'zapier', 'make.com', 'integromat',\n  'email automation', 'ai', 'gpt', 'openai', 'claude', 'llm',\n  'api integration', 'web scraping', 'python', 'javascript',\n  'aws', 'bedrock', 'langchain', 'chatbot', 'data pipeline'\n];\n\nconst item = $input.first().json;\nconst text = `${item.jobTitle} ${item.jobDescription} ${item.skillsRequired}`.toLowerCase();\n\nconst matchedSkills = YOUR_SKILLS.filter(s => text.includes(s.toLowerCase()));\nconst matchScore = matchedSkills.length;\n\nif (matchScore < 2) {\n  return [];\n}\n\nreturn [{\n  json: {\n    ...item,\n    matchScore,\n    matchedSkills\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "8bfae00c-7cd2-498d-8b29-9f33b7abde40",
      "name": "Filter: Client Rating",
      "type": "n8n-nodes-base.code",
      "position": [
        704,
        160
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nconst rating = item.clientRating;\nconst ratingOk = rating === null || rating >= 4.5;\n\nif (!ratingOk) {\n  return [];\n}\n\nreturn [{ json: item }];"
      },
      "typeVersion": 2
    },
    {
      "id": "ad1eaa30-6382-4af5-bffd-0dbfa6941613",
      "name": "Airtable: Check Duplicate",
      "type": "n8n-nodes-base.airtable",
      "position": [
        944,
        160
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_AIRTABLE_BASE_ID",
          "cachedResultUrl": "",
          "cachedResultName": "Leads CRM"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_AIRTABLE_TABLE_ID",
          "cachedResultUrl": "",
          "cachedResultName": "Upwork_jobs"
        },
        "options": {},
        "operation": "search",
        "filterByFormula": "={Job ID}=\"{{ $json.jobId }}\""
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "2a7186ac-e867-47e7-ae2d-2b27b41887a1",
      "name": "Is New Job?",
      "type": "n8n-nodes-base.if",
      "position": [
        1168,
        160
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "dup-check",
              "operator": {
                "type": "string",
                "operation": "notExists",
                "singleValue": true
              },
              "leftValue": "={{ $json.id }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "9ac9216b-39d0-4ce0-bd12-1800fc616ef1",
      "name": "Extract Proposal Text",
      "type": "n8n-nodes-base.code",
      "position": [
        528,
        496
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\nconst proposal = input.message?.content \n  || input.text \n  || input.choices?.[0]?.message?.content\n  || JSON.stringify(input);\n\nconst jobData = $('Filter: Client Rating').first().json;\n\nreturn [{\n  json: {\n    ...jobData,\n    generatedProposal: proposal\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "60e4783e-1a69-4efe-8155-66acbbaacc28",
      "name": "Airtable: Save Proposal",
      "type": "n8n-nodes-base.airtable",
      "position": [
        752,
        496
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_AIRTABLE_BASE_ID",
          "cachedResultUrl": "",
          "cachedResultName": "Leads CRM"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_AIRTABLE_TABLE_ID",
          "cachedResultUrl": "",
          "cachedResultName": "Upwork_jobs"
        },
        "columns": {
          "value": {
            "Budget": "={{ $json.budget }}",
            "Job ID": "={{ $json.jobId }}",
            "Job URL": "={{ $json.jobUrl }}",
            "Job Title": "={{ $json.jobTitle }}",
            "Posted At": "={{ $json.postedAt }}",
            "upwork_url": "={{ $json.upworkUrl }}",
            "AI Proposal": "={{ $json.generatedProposal }}",
            "Match Score": "={{ $json.matchScore }}",
            "Client Rating": "={{ $json.clientRating }}",
            "Matched Skills": "={{ $json.matchedSkills.join(', ') }}",
            "Skills Required": "={{ $json.skillsRequired }}",
            "Client Total Spent": "={{ $json.clientSpent }}"
          },
          "schema": [
            {
              "id": "Job Title",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Job Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Job URL",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Job URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "upwork_url",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "upwork_url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Posted At",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Posted At",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Budget",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Budget",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Skills Required",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Skills Required",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Matched Skills",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Matched Skills",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Match Score",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Match Score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Client Rating",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Client Rating",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Client Total Spent",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Client Total Spent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "AI Proposal",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "AI Proposal",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Job ID",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Job ID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Notes",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Notes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "create"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "b146ef0a-8121-4e03-876c-850ecb5d10c5",
      "name": "Build Slack Message",
      "type": "n8n-nodes-base.code",
      "position": [
        976,
        496
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\nconst job = input.fields;\n\nlet proposal = 'Not available';\ntry {\n  const parsed = JSON.parse(job['AI Proposal']);\n  proposal = parsed.output[0].content[0].text;\n} catch(e) {\n  proposal = job['AI Proposal'] || 'Not available';\n}\n\nconst message = `\ud83c\udfaf *New Upwork Job Match!*\n\n*${job['Job Title']}*\n\n\ud83d\udcb0 *Budget:* ${job['Budget'] || 'Not specified'}\n\ud83d\udcca *Match Score:* ${job['Match Score']} skills matched\n\ud83d\udee0\ufe0f *Matched Skills:* ${job['Matched Skills']}\n\ud83d\udcc5 *Posted:* ${job['Posted At']}\n\ud83d\udccb *Skills Required:* ${job['Skills Required']}\n\ud83d\udd17 *Job Link:* ${job['upwork_url']}\n\n\ud83d\udcdd *AI Generated Proposal:*\n${proposal}\n\n\u2705 Saved in Airtable | Job ID: ${job['Job ID']}`;\n\nreturn [{ json: { ...input, slackMessage: message } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "85039f02-b44c-4e5a-b2a7-d1812cef5639",
      "name": "Slack Notification",
      "type": "n8n-nodes-base.slack",
      "position": [
        1200,
        496
      ],
      "parameters": {
        "text": "={{ $json.slackMessage }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SLACK_CHANNEL_ID",
          "cachedResultName": "channel-name"
        },
        "otherOptions": {}
      },
      "typeVersion": 2.4
    },
    {
      "id": "06ba8fa6-58dd-4e91-bfdf-683ebc4f3a3d",
      "name": "AI: Generate Proposal",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        176,
        496
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini",
          "cachedResultName": "GPT-4O-MINI"
        },
        "options": {
          "maxTokens": 600
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "={{ $json.openAiPayload.messages[0].content }}"
            },
            {
              "content": "={{ $json.openAiPayload.messages[1].content }}"
            }
          ]
        },
        "builtInTools": {}
      },
      "typeVersion": 2.1
    },
    {
      "id": "595a2370-08ed-4443-966a-e68e215792d2",
      "name": "Build OpenAI Payload",
      "type": "n8n-nodes-base.code",
      "position": [
        -48,
        496
      ],
      "parameters": {
        "jsCode": "const job = $('Filter: Client Rating').first().json;\n\nconst userMessage = `Write a proposal for this Upwork job:\n\nJOB TITLE: ${job.jobTitle}\n\nJOB DESCRIPTION:\n${job.jobDescription}\n\nBUDGET: ${job.budget}\nSKILLS REQUIRED: ${job.skillsRequired}\nMATCHED SKILLS: ${Array.isArray(job.matchedSkills) ? job.matchedSkills.join(', ') : (job.matchedSkills || 'Not specified')}\n\nMY PROFILE:\n- Name: [YOUR NAME]\n- Core Skills: [YOUR SKILLS]\n- Experience: [YOUR EXPERIENCE]\n- Style: Direct, technical, solution-first. No buzzwords.\n\nWrite a tailored proposal that:\n1. References a SPECIFIC detail from this exact job post in the opening line\n2. Matches my skills precisely to their requirements\n3. Proposes a concrete first step or quick win\n4. Ends with a clear but soft call-to-action`;\n\nconst payload = {\n  model: \"gpt-4o-mini\",\n  max_tokens: 600,\n  messages: [\n    {\n      role: \"system\",\n      content: \"You are an expert Upwork freelancer proposal writer. You write concise, personalized, high-converting proposals that directly address the client's needs. Never use generic templates. Always: (1) Open with a line proving you read the job post carefully, (2) Demonstrate specific relevant experience with real numbers, (3) Propose a clear approach or solution, (4) End with a soft call-to-action. Keep proposals between 150-250 words. No filler words, no fluff.\"\n    },\n    {\n      role: \"user\",\n      content: userMessage\n    }\n  ]\n};\n\nreturn [{\n  json: {\n    ...job,\n    openAiPayload: payload\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "a97f2b64-ee71-4510-9bab-6c262cef6ba6",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -752,
        64
      ],
      "parameters": {
        "width": 620,
        "height": 620,
        "content": "## How it works\n\nThis workflow monitors Upwork job listings every minute via a Vollna RSS feed. Each new job is parsed to extract the title, description, budget, skills, and a clean Upwork URL. Jobs are scored against your skill list \u2014 only those matching 2 or more of your skills pass through. Low-rated clients are filtered out. Each job is checked against Airtable to avoid processing duplicates.\n\nFor every new qualifying job, GPT-4o-mini writes a personalised 150\u2013250 word proposal referencing details from the actual job post. The proposal and all job data are saved to Airtable with a status of \"New\". A Slack message is sent instantly with the job details, matched skills, and the full proposal ready to copy and submit.\n\n## Setup steps\n\n1. **Vollna** \u2014 Sign up at vollna.com, create a job filter for your skills, and copy the RSS feed URL into the RSS trigger node\n2. **OpenAI** \u2014 Add your API key as an n8n credential and connect it to the AI node\n3. **Airtable** \u2014 Create a base using the schema in the README, then add your Base ID and Table ID to both Airtable nodes\n4. **Slack** \u2014 Create a Slack app with `chat:write` scope, invite it to your channel, and connect it in the Slack node\n5. **Customise** \u2014 Update YOUR_SKILLS in the Filter node and update MY PROFILE in the Build OpenAI Payload node with your actual experience"
      },
      "typeVersion": 1
    },
    {
      "id": "733d0d39-c7c7-45e1-b188-7defe5adf14f",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 532,
        "height": 310,
        "content": "\ud83d\udce1 **Ingest & Parse**\nPolls Vollna RSS every minute. Extracts job title, description, budget, skills, and clean Upwork URL from each item."
      },
      "typeVersion": 1
    },
    {
      "id": "4e5cfecc-424b-4cb6-b008-0b717dd9c01c",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        64
      ],
      "parameters": {
        "color": 7,
        "width": 964,
        "height": 310,
        "content": "\ud83c\udfaf **Filter & Deduplicate**\nNeeds 2+ skill matches. Skips low-rated clients and already-processed jobs."
      },
      "typeVersion": 1
    },
    {
      "id": "6b7ce2c0-3674-423c-b69a-8dd5c028092d",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        384
      ],
      "parameters": {
        "color": 7,
        "width": 1508,
        "height": 294,
        "content": "\ud83e\udd16 **Generate, Save & Notify**\nGPT-4o-mini writes a tailored proposal. Saves to Airtable, sends to Slack."
      },
      "typeVersion": 1
    },
    {
      "id": "83abc871-4136-4fa4-a163-12b53d7021fc",
      "name": "Extract Details from RSS",
      "type": "n8n-nodes-base.code",
      "position": [
        192,
        160
      ],
      "parameters": {
        "jsCode": "const item = $input.item.json;\n\nfunction extractUpworkUrl(vollnaUrl) {\n  try {\n    const split = vollnaUrl.split('url=');\n    if (split.length < 2) return 'no url param found';\n    const raw = split[1];\n    const step1 = raw.replace(/%25/g, '%');\n    const step2 = decodeURIComponent(step1);\n    return step2;\n  } catch(e) {\n    return 'decode error: ' + e.message;\n  }\n}\n\nfunction extractBudget(text) {\n  if (!text) return 'Not specified';\n  const fixed = text.match(/Budget:\\s*\\$?([\\d,]+)/i);\n  const hourly = text.match(/\\$([\\d.]+)\\s*\\/hr/i);\n  if (fixed) return `Fixed: $${fixed[1]}`;\n  if (hourly) return `Hourly: $${hourly[1]}/hr`;\n  return 'Not specified';\n}\n\nfunction extractSkills(text) {\n  if (!text) return '';\n  const match = text.match(/Skills:\\s*([^\\n<]+)/i);\n  return match ? match[1].trim() : '';\n}\n\nfunction extractClientRating(text) {\n  if (!text) return null;\n  const match = text.match(/([\\d.]+)\\s*of\\s*5/i) || text.match(/Rating:\\s*([\\d.]+)/i);\n  return match ? parseFloat(match[1]) : null;\n}\n\nfunction extractClientSpent(text) {\n  if (!text) return null;\n  const match = text.match(/\\$([\\d,]+)\\+?\\s*spent/i) || text.match(/\\$([\\d,]+)\\+?\\s*total spent/i);\n  return match ? match[1].replace(/,/g, '') : null;\n}\n\nfunction generateJobId(url) {\n  const match = url?.match(/~([a-zA-Z0-9]+)/);\n  return match ? match[1] : url;\n}\n\nconst description = item.contentSnippet || item.content || item.summary || '';\nconst guid = item.guid || item.link || '';\n\nreturn [{\n  json: {\n    jobTitle: item.title || 'Untitled',\n    jobUrl: item.link || item.url || '',\n    jobDescription: description,\n    postedAt: item.pubDate || item.isoDate || new Date().toISOString(),\n    budget: extractBudget(description),\n    skillsRequired: extractSkills(description),\n    clientRating: extractClientRating(description),\n    clientSpent: extractClientSpent(description),\n    jobId: generateJobId(item.link || item.url || guid),\n    rawGuid: guid,\n    upworkUrl: extractUpworkUrl(guid)\n  }\n}];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "8ceacd4c-087e-4e99-98c6-eb97b50b96f2",
  "connections": {
    "Is New Job?": {
      "main": [
        [
          {
            "node": "Build OpenAI Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Slack Message": {
      "main": [
        [
          {
            "node": "Slack Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build OpenAI Payload": {
      "main": [
        [
          {
            "node": "AI: Generate Proposal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter: Skills Match": {
      "main": [
        [
          {
            "node": "Filter: Client Rating",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI: Generate Proposal": {
      "main": [
        [
          {
            "node": "Extract Proposal Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Proposal Text": {
      "main": [
        [
          {
            "node": "Airtable: Save Proposal",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter: Client Rating": {
      "main": [
        [
          {
            "node": "Airtable: Check Duplicate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable: Save Proposal": {
      "main": [
        [
          {
            "node": "Build Slack Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Details from RSS": {
      "main": [
        [
          {
            "node": "Filter: Skills Match",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Airtable: Check Duplicate": {
      "main": [
        [
          {
            "node": "Is New Job?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RSS Feed - n8n & Automation": {
      "main": [
        [
          {
            "node": "Extract Details from RSS",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}